File Coverage

blib/lib/Astro/App/Satpass2/Format/Template.pm
Criterion Covered Total %
statement 221 256 86.3
branch 63 106 59.4
condition 11 24 45.8
subroutine 43 48 89.5
pod 11 11 100.0
total 349 445 78.4


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