File Coverage

blib/lib/Astro/App/Satpass2/Format/Template.pm
Criterion Covered Total %
statement 208 236 88.1
branch 60 100 60.0
condition 14 27 51.8
subroutine 42 45 93.3
pod 11 11 100.0
total 335 419 79.9


line stmt bran cond sub pod time code
1             package Astro::App::Satpass2::Format::Template;
2              
3 8     8   1767 use strict;
  8         25  
  8         277  
4 8     8   75 use warnings;
  8         20  
  8         244  
5              
6 8     8   53 use parent qw{ Astro::App::Satpass2::Format };
  8         18  
  8         76  
7              
8 8     8   433 use Astro::App::Satpass2::Locale qw{ __localize };
  8         18  
  8         450  
9             # use Astro::App::Satpass2::FormatValue;
10 8     8   3801 use Astro::App::Satpass2::FormatValue::Formatter;
  8         26  
  8         289  
11 8         883 use Astro::App::Satpass2::Utils qw{
12             instance
13             ARRAY_REF
14             HASH_REF
15             SCALAR_REF
16             @CARP_NOT
17 8     8   57 };
  8         28  
18 8     8   3725 use Astro::App::Satpass2::Wrap::Array;
  8         24  
  8         340  
19 8     8   66 use Astro::Coord::ECI::TLE 0.059 qw{ :constants };
  8         143  
  8         1508  
20 8         546 use Astro::Coord::ECI::Utils 0.059 qw{
21             deg2rad embodies julianday PI rad2deg TWOPI
22 8     8   113 };
  8         125  
23 8     8   54 use Clone qw{ };
  8         17  
  8         212  
24 8     8   60 use POSIX qw{ floor };
  8         33  
  8         64  
25 8     8   5312 use Template;
  8         164482  
  8         279  
26 8     8   68 use Template::Provider;
  8         21  
  8         151  
27 8     8   748 use Text::Abbrev;
  8         65  
  8         545  
28 8     8   53 use Text::Wrap qw{ wrap };
  8         45  
  8         535  
29              
30             our $VERSION = '0.051';
31              
32 8     8   62 use constant FORMAT_VALUE => 'Astro::App::Satpass2::FormatValue';
  8         28  
  8         19881  
33              
34             sub new {
35 9     9 1 6224 my ($class, @args) = @_;
36 9         75 my $self = $class->SUPER::new( @args );
37              
38             # As of 0.020_002 the template definitions are in the
39             # locale system. The attribute simply holds modifications.
40 9         27 $self->{canned_template} = {};
41              
42 9         63 $self->_new_tt( $self->permissive() );
43              
44 9         26 $self->{default} = {};
45 9         40 $self->{formatter_method} = {};
46              
47 9         67 return $self;
48             }
49              
50             sub _new_tt {
51 10     10   33 my ( $self, $permissive ) = @_;
52              
53 10 50       104 $self->{tt} = Template->new(
54             {
55             LOAD_TEMPLATES => [
56             Template::Provider->new(
57             ABSOLUTE => $permissive,
58             RELATIVE => $permissive,
59             ),
60             ],
61             }
62             ) or $self->weep(
63             "Failed to instantate tt: $Template::ERROR" );
64              
65 10         201602 return;
66             }
67              
68             sub add_formatter_method {
69             # TODO I want the arguments to be ( $self, $fmtr ), but for the
70             # moment I have to live with an unreleased version that passed the
71             # name as the first argument. I will go to the desired signature as
72             # soon as I get this version installed on my own machine.
73 1     1 1 6 my ( $self, @arg ) = @_;
74 1 50       6 my $fmtr = HASH_REF eq ref $arg[0] ? $arg[0] : $arg[1];
75 1 50       6 HASH_REF eq ref $fmtr
76             or $self->wail(
77             'Formatter definition must be a HASH reference' );
78             defined( my $fmtr_name = $fmtr->{name} )
79 1 50       5 or $self->wail(
80             'Formatter definition must have {name} defined' );
81             $self->{formatter_method}{$fmtr_name}
82             and $self->{warner}->wail(
83 1 50       6 "Formatter method $fmtr_name already exists" );
84             FORMAT_VALUE->can( $fmtr_name )
85             and $self->{warner}->wail(
86 1 50       18 "Formatter $fmtr_name can not override built-in formatter" );
87 1         11 $self->{formatter_method}{$fmtr_name} =
88             Astro::App::Satpass2::FormatValue::Formatter->new( $fmtr );
89 1         4 return $self;
90             }
91              
92             sub attribute_names {
93 17     17 1 68 my ( $self ) = @_;
94 17         100 return ( $self->SUPER::attribute_names(),
95             qw{ permissive },
96             );
97             }
98              
99             sub config {
100 0     0 1 0 my ( $self, %args ) = @_;
101 0         0 my @data = $self->SUPER::config( %args );
102              
103             # TODO support for the {default} key.
104              
105 0 0       0 foreach my $name (
106             sort $args{changes} ?
107 0         0 keys %{ $self->{canned_template} } :
108             $self->__list_templates()
109             ) {
110             push @data, [ template => $name,
111 0         0 $self->{canned_template}{$name} ];
112             }
113              
114 0 0       0 return wantarray ? @data : \@data;
115             }
116              
117             # Return the names of all known templates, in no particular order. No
118             # arguments other than the invocant.
119             sub __list_templates {
120 24     24   81 my ( $self ) = @_;
121 72         127 return ( _uniq( map { keys %{ $_ } } $self->{canned_template},
  72         573  
122 24         138 __localize(
123             text => '+template',
124             default => {},
125             ) ) );
126             }
127              
128             {
129             my %decoder = (
130             template => sub {
131             my ( $self, $method, @args ) = @_;
132              
133             =begin comment
134              
135             1 == @args
136             and return ( @args, $self->$method( @args ) );
137             $self->$method( @args );
138             return $self;
139              
140             =end comment
141              
142             =cut
143              
144             1 == @args
145             or return $self->$method( @args );
146             return ( @args, $self->$method( @args ) );
147             },
148             );
149              
150             sub decode {
151 7     7 1 17 my ( $self, $method, @args ) = @_;
152 7 50       47 my $dcdr = $decoder{$method}
153             or return $self->SUPER::decode( $method, @args );
154 0         0 goto $dcdr;
155             }
156             }
157              
158             sub __default {
159 302     302   627 my ( $self, @arg ) = @_;
160 302 50       1388 @arg or return $self->{default};
161 0         0 my $action = shift @arg;
162 0 0       0 @arg or return $self->{default}{$action};
163 0         0 my $attrib = shift @arg;
164             defined $attrib
165 0 0       0 or return delete $self->{default}{$action};
166 0 0       0 @arg or return $self->{default}{$action}{$attrib};
167 0         0 my $value = shift @arg;
168             defined $value
169 0 0       0 or return delete $self->{default}{$action}{$attrib};
170 0         0 $self->{default}{$action}{$attrib} = $value;
171 0         0 return $value;
172             }
173              
174             sub format : method { ## no critic (ProhibitBuiltInHomonyms)
175 62     62 1 4114 my ( $self, %data ) = @_;
176              
177             exists $data{data}
178             and $data{data} = $self->_wrap(
179             data => $data{data},
180             report => $data{template},
181 62 100       489 );
182              
183 62 100       392 _is_format() and return $data{data};
184              
185             my $tplt = delete $data{template}
186 61 50       335 or $self->wail( 'template argument is required' );
187              
188 61 100       265 my $tplt_name = SCALAR_REF eq ref $tplt ? ${ $tplt } : $tplt;
  1         4  
189              
190 61   33     343 $data{default} ||= $self->__default();
191              
192             $data{instantiate} = sub {
193 0     0   0 my @args = @_;
194 0         0 my $class = Astro::App::Satpass2::Utils::load_package( @args );
195 0         0 return $class->new();
196 61         463 };
197              
198 61   33     448 $data{provider} ||= $self->provider();
199              
200 61 50       184 if ( $data{time} ) {
201             ref $data{time}
202             or $data{time} = $self->_wrap(
203             data => { time => $data{time} },
204 0 0       0 report => $tplt_name,
205             );
206             } else {
207 61         330 $data{time} = $self->_wrap(
208             data => { time => time },
209             report => $tplt_name,
210             );
211             }
212              
213 61         340 my $value_formatter = $self->value_formatter();
214              
215             $data{title} = $self->_wrap(
216             default => $data{default},
217 61         300 report => $tplt_name,
218             );
219             $data{TITLE_GRAVITY_BOTTOM} =
220 61         510 $value_formatter->TITLE_GRAVITY_BOTTOM;
221             $data{TITLE_GRAVITY_TOP} =
222 61         280 $value_formatter->TITLE_GRAVITY_TOP;
223              
224             local $Template::Stash::LIST_OPS->{bodies} = sub {
225 0     0   0 my ( $list ) = @_;
226 0         0 return [ map { $_->body() } @{ $list } ];
  0         0  
  0         0  
227 61         570 };
228              
229             local $Template::Stash::LIST_OPS->{events} = sub {
230 3     3   64 my @args = @_;
231 3         24 return $self->_all_events( $args[0] );
232 61         398 };
233              
234             local $Template::Stash::LIST_OPS->{fixed_width} = sub {
235 2     2   11 my ( $list, $value ) = @_;
236 2         7 foreach my $item ( @{ $list } ) {
  2         8  
237 10 50       40 my $code = $item->can( 'fixed_width' )
238             or next;
239 10         28 $code->( $item, $value );
240             }
241 2         17 return;
242 61         401 };
243              
244             $data{localize} = sub {
245 28     28   81406 return _localize( $tplt_name, @_ );
246 61         389 };
247              
248             # NOTE - must come after $data{localize} because
249             # $data{format_detail} uses $data{localize}
250             $data{format_detail} = sub {
251 173     173   9126 my ( $kind, $evt ) = @_;
252              
253 173 50       601 instance( $evt, 'Astro::App::Satpass2::FormatValue' )
254             or return;
255 173 50       677 defined ( my $type = $evt->$kind( width => '' ) )
256             or return;
257 173         689 $type =~ s/ \s+ \z //smx;
258              
259 173         540 foreach my $name ( "$kind:$type", $kind ) {
260 329 100       1138 defined ( my $tplt = $self->template( "$tplt:$name" ) )
261             or next;
262             my $output = $self->_process( \$tplt,
263             evt => $evt,
264             localize => $data{localize},
265             sp => $data{sp},
266 173         755 );
267              
268 173         517 chomp $output;
269 173         1169 return $output;
270             }
271 0         0 return __localize(
272             text => [ '+template', "$tplt:$kind" ],
273             );
274 61         453 };
275              
276 61         557 my $output = $self->_process( $tplt, %data );
277              
278             # TODO would love to use \h here, but that needs 5.10.
279 60         1348 $output =~ s/ [ \t]+ (?= \n ) //sxmg;
280             $data{title}->title_gravity() eq $data{TITLE_GRAVITY_BOTTOM}
281 60 100       425 and $output =~ s/ \A \n //smx;
282              
283 60         1378 return $output;
284             }
285              
286             sub gmt {
287 57     57 1 219 my ( $self, @args ) = @_;
288 57 100       212 if ( @args ) {
289 3         18 $self->time_formatter()->gmt( @args );
290 3         24 return $self->SUPER::gmt( @args );
291             } else {
292 54         270 return $self->SUPER::gmt();
293             }
294             }
295              
296             sub local_coord {
297 207     207 1 440 my ( $self, @args ) = @_;
298 207 100       535 if ( @args ) {
299 12         47 my $val = $args[0];
300 12 100       121 defined $val
301             or $val = $self->DEFAULT_LOCAL_COORD;
302              
303 12 50       391 defined $self->template( $val )
304             or $self->wail(
305             'Unknown local coordinate specification', $val );
306              
307 12         91 return $self->SUPER::local_coord( @args );
308             } else {
309 195         725 return $self->SUPER::local_coord();
310             }
311             }
312              
313             sub permissive {
314 10     10 1 695 my ( $self, @args ) = @_;
315 10 100       40 if ( @args ) {
316 1 50 25     11 if ( $self->{permissive} xor $args[0] ) {
317 1         8 $self->_new_tt( $args[0] );
318             }
319 1         4 $self->{permissive} = $args[0];
320 1         4 return $self;
321             } else {
322 9         65 return $self->{permissive};
323             }
324             }
325              
326             sub template {
327 620     620 1 1336 my ( $self, $name, @value ) = @_;
328 620 50       1236 defined $name
329             or $self->wail( 'Template name not specified' );
330              
331 620 100       1320 if ( @value ) {
332 3         9 my $tplt_text;
333 3 50 33     22 if ( ! defined $value[0]
      33        
334             || defined( $tplt_text = __localize(
335             text => '+template',
336             default => $value[0] )
337             )
338             && $value[0] eq $tplt_text
339             ) {
340 0         0 delete $self->{canned_template}{$name};
341             } else {
342 3         13 $self->{canned_template}{$name} = $value[0];
343             }
344              
345 3         12 return $self;
346             } else {
347             defined $self->{canned_template}{$name}
348 617 100       1631 and return $self->{canned_template}{$name};
349 613         2246 return __localize(
350             text => [ '+template', $name ],
351             );
352             }
353             }
354              
355             sub tz {
356 16     16 1 59 my ( $self, @args ) = @_;
357 16 50       81 if ( @args ) {
358 16         83 my $tf = $self->time_formatter();
359             # We go through the following because the time formatter may
360             # modify the zone (e.g. if it's using DateTime, zones are
361             # case-sensitive so we may have done case conversion before
362             # storing). We want this object to have the time formatter's
363             # version of the zone.
364 16         151 $tf->tz( @args );
365 16         55 return $self->SUPER::tz( $tf->tz() );
366             } else {
367 0         0 return $self->SUPER::tz();
368             }
369             }
370              
371             sub _all_events {
372 3     3   15 my ( $self, $data ) = @_;
373 3 50       22 ARRAY_REF eq ref $data or return;
374              
375 3         7 my @events;
376 3         9 foreach my $pass ( @{ $data } ) {
  3         19  
377 3         36 push @events, $pass->__raw_events();
378             }
379 3 50       14 @events or return;
380 3         28 @events = sort { $a->{time} <=> $b->{time} } @events;
  16         42  
381              
382 3         14 return [ map { $self->_wrap( data => $_ ) } @events ];
  13         37  
383             }
384              
385             # _is_format()
386             #
387             # Returns true if the format() method is above us on the call
388             # stack, otherwise returns false.
389              
390 8     8   90 use constant REPORT_CALLER => __PACKAGE__ . '::format';
  8         19  
  8         6880  
391             sub _is_format {
392 62     62   185 my $level = 2; # Start with caller's caller.
393 62         830 while ( my @info = caller( $level ) ) {
394 261 100       630 REPORT_CALLER eq $info[3]
395             and return $level;
396 260         1951 $level++;
397             }
398 61         265 return;
399             }
400              
401             sub _localize {
402 28     28   78 my ( $report, $source, $default ) = @_;
403 28 50       82 defined $default
404             or $default = $source;
405 28 0       70 defined $report
    50          
406             or return defined $source ? $source : $default;
407              
408 28         117 return scalar __localize(
409             text => [ "-$report", 'string', $source ],
410             default => $source,
411             );
412             }
413              
414             sub _process {
415 449     449   2111 my ( $self, $tplt, %arg ) = @_;
416             ARRAY_REF eq ref $arg{arg}
417             and $arg{arg} = Astro::App::Satpass2::Wrap::Array->new(
418 449 100       2340 $arg{arg} );
419 449         750 my $output;
420 449         940 my $tt = $self->{tt};
421              
422 449         687 my $tplt_text;
423 449 100 100     1774 not ref $tplt
424             and defined( $tplt_text = $self->template( $tplt ) )
425             and $tplt = \$tplt_text;
426              
427 449 100       1897 $tt->process( $tplt, \%arg, \$output )
428             or $self->wail( $tt->error() );
429 448         117916 return $output;
430             }
431              
432             # Cribbed shamelessly from List::MoreUtils. The author reserves the
433             # right to relocate, rename or otherwise mung with this without notice
434             # to anyone. Caveat user.
435             sub _uniq {
436 25     25   80 my %found;
437 25         76 return ( grep { ! $found{$_}++ } @_ );
  679         1752  
438             }
439              
440             sub _wrap {
441 517     517   1932 my ( $self, %arg ) = @_;
442              
443 517         991 my $data = $arg{data};
444 517         879 my $default = $arg{default};
445 517         933 my $report = $arg{report};
446              
447 517         998 my $title = ! $data;
448 517   100     2019 $data ||= {};
449 517   66     1539 $default ||= $self->__default();
450              
451 517 50 66     1671 if ( instance( $data, FORMAT_VALUE ) ) {
    100          
    100          
    50          
452             # Do nothing
453             } elsif ( ! defined $data || HASH_REF eq ref $data ) {
454 449         1574 my $value_formatter = $self->value_formatter();
455             $data = $value_formatter->new(
456             data => $data,
457             default => $default,
458             date_format => $self->date_format(),
459             desired_equinox_dynamical =>
460             $self->desired_equinox_dynamical(),
461             provider => $self->provider(),
462             round_time => $self->round_time(),
463             time_format => $self->time_format(),
464             time_formatter => $self->time_formatter(),
465             local_coordinates => sub {
466 195     195   488 my ( $data, @arg ) = @_;
467             return $self->_process( $self->local_coord(),
468             data => $data,
469             arg => \@arg,
470             title => $self->_wrap(
471             default => $default,
472             report => $report,
473             ),
474             localize => sub {
475 0         0 return _localize( $report, @_ );
476             },
477 195         700 );
478             },
479             list_formatter => sub {
480 20     20   47 my ( $data, @arg ) = @_;
481 20         68 my $body = $data->body();
482 20 100       317 my $list_type = $body ? $body->__list_type() : 'inertial';
483 20         156 return $self->_process( "list_$list_type",
484             data => $data,
485             arg => \@arg,
486             title => $self->_wrap(
487             default => $default,
488             report => $report,
489             ),
490             );
491             },
492 449         1482 report => $report,
493             title => $title,
494             warner => $self->warner(),
495             );
496 449         1052 $data->add_formatter_method( values %{ $self->{formatter_method} } );
  449         2019  
497             } elsif ( ARRAY_REF eq ref $data ) {
498 45         148 $data = [ map { $self->_wrap( data => $_, report => $report ) } @{ $data } ];
  85         335  
  45         162  
499             } elsif ( embodies( $data, 'Astro::Coord::ECI' ) ) {
500 23         796 $data = $self->_wrap(
501             data => { body => $data },
502             report => $report,
503             );
504             }
505              
506 517         2704 return $data;
507             }
508              
509             __PACKAGE__->create_attribute_methods();
510              
511             1;
512              
513             __END__