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   142798 use strict;
  8         18  
  8         277  
4 8     8   31 use warnings;
  8         13  
  8         491  
5              
6 8     8   586 use parent qw{ Astro::App::Satpass2::Format };
  8         342  
  8         56  
7              
8 8     8   388 use Astro::App::Satpass2::Locale qw{ __localize };
  8         13  
  8         394  
9             # use Astro::App::Satpass2::FormatValue;
10 8     8   3269 use Astro::App::Satpass2::FormatValue::Formatter;
  8         21  
  8         269  
11 8         822 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         10  
19 8     8   3993 use Astro::App::Satpass2::Wrap::Array;
  8         21  
  8         335  
20 8     8   3489 use Astro::App::Satpass2::Format::Template::Provider;
  8         23  
  8         394  
21 8     8   44 use Astro::Coord::ECI::TLE 0.059 qw{ :constants };
  8         216  
  8         1558  
22 8         467 use Astro::Coord::ECI::Utils 0.059 qw{
23             deg2rad embodies julianday PI rad2deg TWOPI
24 8     8   45 };
  8         101  
25 8     8   35 use Clone qw{ };
  8         11  
  8         106  
26 8     8   26 use POSIX qw{ floor };
  8         10  
  8         98  
27 8     8   3477 use Template;
  8         27560  
  8         271  
28 8     8   602 use Text::Abbrev;
  8         51  
  8         419  
29 8     8   37 use Text::Wrap qw{ wrap };
  8         13  
  8         516  
30              
31             our $VERSION = '0.058';
32              
33 8     8   40 use constant FORMAT_VALUE => 'Astro::App::Satpass2::FormatValue';
  8         14  
  8         17770  
34              
35             sub new {
36 9     9 1 3692 my ($class, @args) = @_;
37 9         48 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         23 $self->{canned_template} = {};
42              
43 9         35 $self->_new_tt( $self->permissive() );
44              
45 9         28 $self->{default} = {};
46 9         22 $self->{formatter_method} = {};
47              
48 9         77 return $self;
49             }
50              
51             sub _new_tt {
52 10     10   21 my ( $self, $permissive ) = @_;
53              
54 10 50       1165 $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         158306 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 5 my ( $self, @arg ) = @_;
75 1 50       6 my $fmtr = HASH_REF eq ref $arg[0] ? $arg[0] : $arg[1];
76 1 50       6 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       7 or $self->wail(
81             'Formatter definition must have {name} defined' );
82             $self->{formatter_method}{$fmtr_name}
83             and $self->{warner}->wail(
84 1 50       5 "Formatter method $fmtr_name already exists" );
85             FORMAT_VALUE->can( $fmtr_name )
86             and $self->{warner}->wail(
87 1 50       62 "Formatter $fmtr_name can not override built-in formatter" );
88 1         24 $self->{formatter_method}{$fmtr_name} =
89             Astro::App::Satpass2::FormatValue::Formatter->new( $fmtr );
90 1         6 return $self;
91             }
92              
93             sub attribute_names {
94 17     17 1 212 my ( $self ) = @_;
95 17         68 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   65 my ( $self ) = @_;
122 72         84 return ( _uniq( map { keys %{ $_ } } $self->{canned_template},
  72         438  
123 24         100 __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 15 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   408 my ( $self, @arg ) = @_;
161 302 50       941 @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 3641 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       451 );
183              
184 62 100       557 _is_format() and return $data{data};
185              
186             my $tplt = delete $data{template}
187 61 50       254 or $self->wail( 'template argument is required' );
188              
189 61 100       181 my $tplt_name = SCALAR_REF eq ref $tplt ? ${ $tplt } : $tplt;
  1         2  
190              
191 61   33     265 $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         288 };
198              
199 61   33     350 $data{provider} ||= $self->provider();
200              
201 61 50       148 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         241 $data{time} = $self->_wrap(
209             data => { time => time },
210             report => $tplt_name,
211             );
212             }
213              
214 61         174 my $value_formatter = $self->value_formatter();
215              
216             $data{title} = $self->_wrap(
217             default => $data{default},
218 61         164 report => $tplt_name,
219             );
220             $data{TITLE_GRAVITY_BOTTOM} =
221 61         259 $value_formatter->TITLE_GRAVITY_BOTTOM;
222             $data{TITLE_GRAVITY_TOP} =
223 61         210 $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         363 };
229              
230             local $Template::Stash::LIST_OPS->{events} = sub {
231 3     3   66 my @args = @_;
232 3         1111 return $self->_all_events( $args[0] );
233 61         288 };
234              
235             local $Template::Stash::LIST_OPS->{fixed_width} = sub {
236 2     2   5 my ( $list, $value ) = @_;
237 2         4 foreach my $item ( @{ $list } ) {
  2         3  
238 10 50       20 my $code = $item->can( 'fixed_width' )
239             or next;
240 10         15 $code->( $item, $value );
241             }
242 2         7 return;
243 61         288 };
244              
245             local $Template::Stash::LIST_OPS->{first_tle} = sub {
246 29     29   78 my ( $list ) = @_;
247 29         83 foreach my $item ( @{ $list } ) {
  29         68  
248 29 50       149 embodies( $item->body(), 'Astro::Coord::ECI::TLE' )
249             and return $item;
250             }
251 0         0 return;
252 61         286 };
253              
254 61         95 local $Template::Stash::LIST_OPS->{to_json} = do {
255 61         107 local $@ = undef;
256             eval {
257 61         293 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         502 } || sub {
265 0     0   0 $self->wail( 'Module JSON could not be loaded' );
266 61 50       97 };
267             };
268              
269             $data{localize} = sub {
270 28     28   59725 return _localize( $tplt_name, @_ );
271 61         309 };
272              
273             # NOTE - must come after $data{localize} because
274             # $data{format_detail} uses $data{localize}
275             $data{format_detail} = sub {
276 173     173   6724 my ( $kind, $evt ) = @_;
277              
278 173 50       802 instance( $evt, FORMAT_VALUE )
279             or return;
280 173 50       582 defined ( my $type = $evt->$kind( width => '' ) )
281             or return;
282 173         626 $type =~ s/ \s+ \z //smx;
283              
284 173         487 foreach my $name ( "$kind:$type", $kind ) {
285 329 100       946 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         836 );
292              
293 173         508 chomp $output;
294 173         989 return $output;
295             }
296 0         0 return __localize(
297             text => [ '+template', "$tplt:$kind" ],
298             );
299 61         313 };
300              
301 61         365 my $output = $self->_process( $tplt, %data );
302              
303             # TODO would love to use \h here, but that needs 5.10.
304 60         1164 $output =~ s/ [ \t]+ (?= \n ) //sxmg;
305             $data{title}->title_gravity() eq $data{TITLE_GRAVITY_BOTTOM}
306 60 100       297 and $output =~ s/ \A \n //smx;
307              
308 60         1627 return $output;
309             }
310              
311             sub gmt {
312 57     57 1 113 my ( $self, @args ) = @_;
313 57 100       145 if ( @args ) {
314 3         12 $self->time_formatter()->gmt( @args );
315 3         20 return $self->SUPER::gmt( @args );
316             } else {
317 54         243 return $self->SUPER::gmt();
318             }
319             }
320              
321             sub local_coord {
322 207     207 1 411 my ( $self, @args ) = @_;
323 207 100       479 if ( @args ) {
324 12         22 my $val = $args[0];
325 12 100       77 defined $val
326             or $val = $self->DEFAULT_LOCAL_COORD;
327              
328 12 50       32 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         824 return $self->SUPER::local_coord();
335             }
336             }
337              
338             sub permissive {
339 10     10 1 469 my ( $self, @args ) = @_;
340 10 100       23 if ( @args ) {
341 1 50 25     8 if ( $self->{permissive} xor $args[0] ) {
342 1         4 $self->_new_tt( $args[0] );
343             }
344 1         3 $self->{permissive} = $args[0];
345 1         3 return $self;
346             } else {
347 9         50 return $self->{permissive};
348             }
349             }
350              
351             sub template {
352 620     620 1 1151 my ( $self, $name, @value ) = @_;
353 620 50       1054 defined $name
354             or $self->wail( 'Template name not specified' );
355              
356 620 100       1032 if ( @value ) {
357 3         5 my $tplt_text;
358 3 50 33     26 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         14 $self->{canned_template}{$name} = $value[0];
368             }
369              
370 3         10 return $self;
371             } else {
372             defined $self->{canned_template}{$name}
373 617 100       1405 and return $self->{canned_template}{$name};
374 613         1828 return __localize(
375             text => [ '+template', $name ],
376             );
377             }
378             }
379              
380             sub tz {
381 16     16 1 44 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         87 $tf->tz( @args );
390 16         32 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   9 my ( $self, $data ) = @_;
398 3 50       12 ARRAY_REF eq ref $data or return;
399              
400 3         5 my @events;
401 3         5 foreach my $pass ( @{ $data } ) {
  3         21  
402 3         14 push @events, $pass->__raw_events();
403             }
404 3 50       9 @events or return;
405 3         20 @events = sort { $a->{time} <=> $b->{time} } @events;
  16         30  
406              
407 3         6 return [ map { $self->_wrap( data => $_ ) } @events ];
  13         29  
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   59 use constant REPORT_CALLER => __PACKAGE__ . '::format';
  8         15  
  8         6146  
416             sub _is_format {
417 62     62   98 my $level = 2; # Start with caller's caller.
418 62         457 while ( my @info = caller( $level ) ) {
419 343 100       504 REPORT_CALLER eq $info[3]
420             and return $level;
421 342         1559 $level++;
422             }
423 61         146 return;
424             }
425              
426             sub _localize {
427 28     28   63 my ( $report, $source, $default ) = @_;
428 28 50       62 defined $default
429             or $default = $source;
430 28 0       49 defined $report
    50          
431             or return defined $source ? $source : $default;
432              
433 28         104 return scalar __localize(
434             text => [ "-$report", 'string', $source ],
435             default => $source,
436             );
437             }
438              
439             sub _process {
440 449     449   1875 my ( $self, $tplt, %arg ) = @_;
441             ARRAY_REF eq ref $arg{arg}
442             and $arg{arg} = Astro::App::Satpass2::Wrap::Array->new(
443 449 100       1949 $arg{arg} );
444 449         563 my $output;
445 449         777 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       754 unless ( ref $tplt ) {
455 275         763 while ( defined( my $tplt_text = $self->template( $tplt ) ) ) {
456 272 50       738 if ( $tplt_text =~ m/\A %% \s* include \s+ ( \w+ ) \s* \z /smxi ) {
457 0         0 $tplt = $1;
458             } else {
459 272         475 $tplt = \$tplt_text;
460 272         476 last;
461             }
462             }
463             }
464              
465 449 100       1932 $tt->process( $tplt, \%arg, \$output )
466             or $self->wail( $tt->error() );
467 448         87271 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   557 my %found;
475 25         54 return ( grep { ! $found{$_}++ } @_ );
  727         1319  
476             }
477              
478             sub _wrap {
479 517     517   1449 my ( $self, %arg ) = @_;
480              
481 517         832 my $data = $arg{data};
482 517         741 my $default = $arg{default};
483 517         701 my $report = $arg{report};
484              
485 517         1041 my $title = ! $data;
486 517   100     1491 $data ||= {};
487 517   66     1350 $default ||= $self->__default();
488              
489 517 50 66     1585 if ( instance( $data, FORMAT_VALUE ) ) {
    100          
    100          
    50          
490             # Do nothing
491             } elsif ( ! defined $data || HASH_REF eq ref $data ) {
492 449         1216 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   492 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         676 );
516             },
517             list_formatter => sub {
518 20     20   33 my ( $data, @arg ) = @_;
519 20         53 my $body = $data->body();
520 20 100       287 my $list_type = $body ? $body->__list_type() : 'inertial';
521 20         123 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         1319 report => $report,
531             title => $title,
532             warner => $self->warner(),
533             );
534 449         880 $data->add_formatter_method( values %{ $self->{formatter_method} } );
  449         1480  
535             } elsif ( ARRAY_REF eq ref $data ) {
536 45         74 $data = [ map { $self->_wrap( data => $_, report => $report ) } @{ $data } ];
  85         227  
  45         103  
537             } elsif ( embodies( $data, 'Astro::Coord::ECI' ) ) {
538 23         621 $data = $self->_wrap(
539             data => { body => $data },
540             report => $report,
541             );
542             }
543              
544 517         1963 return $data;
545             }
546              
547             __PACKAGE__->create_attribute_methods();
548              
549             1;
550              
551             __END__