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   239569 use strict;
  8         21  
  8         353  
4 8     8   44 use warnings;
  8         17  
  8         585  
5              
6 8     8   596 use parent qw{ Astro::App::Satpass2::Format };
  8         377  
  8         63  
7              
8 8     8   487 use Astro::App::Satpass2::Locale qw{ __localize };
  8         20  
  8         518  
9             # use Astro::App::Satpass2::FormatValue;
10 8     8   4264 use Astro::App::Satpass2::FormatValue::Formatter;
  8         59  
  8         415  
11 8         1097 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   56 };
  8         16  
19 8     8   5199 use Astro::App::Satpass2::Wrap::Array;
  8         28  
  8         470  
20 8     8   4857 use Astro::App::Satpass2::Format::Template::Provider;
  8         54  
  8         552  
21 8     8   76 use Astro::Coord::ECI::TLE 0.059 qw{ :constants };
  8         181  
  8         1980  
22 8         676 use Astro::Coord::ECI::Utils 0.059 qw{
23             deg2rad embodies julianday PI rad2deg TWOPI
24 8     8   62 };
  8         173  
25 8     8   57 use Clone qw{ };
  8         15  
  8         199  
26 8     8   41 use POSIX qw{ floor };
  8         13  
  8         78  
27 8     8   4611 use Template;
  8         35059  
  8         353  
28 8     8   969 use Text::Abbrev;
  8         68  
  8         506  
29 8     8   46 use Text::Wrap qw{ wrap };
  8         16  
  8         607  
30              
31             our $VERSION = '0.057_01';
32              
33 8     8   41 use constant FORMAT_VALUE => 'Astro::App::Satpass2::FormatValue';
  8         15  
  8         20004  
34              
35             sub new {
36 9     9 1 6107 my ($class, @args) = @_;
37 9         58 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         24 $self->{canned_template} = {};
42              
43 9         47 $self->_new_tt( $self->permissive() );
44              
45 9         35 $self->{default} = {};
46 9         26 $self->{formatter_method} = {};
47              
48 9         82 return $self;
49             }
50              
51             sub _new_tt {
52 10     10   21 my ( $self, $permissive ) = @_;
53              
54 10 50       170 $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         196942 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       5 or $self->wail(
81             'Formatter definition must have {name} defined' );
82             $self->{formatter_method}{$fmtr_name}
83             and $self->{warner}->wail(
84 1 50       6 "Formatter method $fmtr_name already exists" );
85             FORMAT_VALUE->can( $fmtr_name )
86             and $self->{warner}->wail(
87 1 50       50 "Formatter $fmtr_name can not override built-in formatter" );
88 1         13 $self->{formatter_method}{$fmtr_name} =
89             Astro::App::Satpass2::FormatValue::Formatter->new( $fmtr );
90 1         5 return $self;
91             }
92              
93             sub attribute_names {
94 17     17 1 226 my ( $self ) = @_;
95 17         86 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   66 my ( $self ) = @_;
122 72         103 return ( _uniq( map { keys %{ $_ } } $self->{canned_template},
  72         524  
123 24         134 __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       39 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   642 my ( $self, @arg ) = @_;
161 302 50       1411 @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 5397 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       534 );
183              
184 62 100       247 _is_format() and return $data{data};
185              
186             my $tplt = delete $data{template}
187 61 50       246 or $self->wail( 'template argument is required' );
188              
189 61 100       242 my $tplt_name = SCALAR_REF eq ref $tplt ? ${ $tplt } : $tplt;
  1         3  
190              
191 61   33     403 $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         406 };
198              
199 61   33     478 $data{provider} ||= $self->provider();
200              
201 61 50       213 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         278 $data{time} = $self->_wrap(
209             data => { time => time },
210             report => $tplt_name,
211             );
212             }
213              
214 61         221 my $value_formatter = $self->value_formatter();
215              
216             $data{title} = $self->_wrap(
217             default => $data{default},
218 61         292 report => $tplt_name,
219             );
220             $data{TITLE_GRAVITY_BOTTOM} =
221 61         311 $value_formatter->TITLE_GRAVITY_BOTTOM;
222             $data{TITLE_GRAVITY_TOP} =
223 61         266 $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         599 };
229              
230             local $Template::Stash::LIST_OPS->{events} = sub {
231 3     3   94 my @args = @_;
232 3         21 return $self->_all_events( $args[0] );
233 61         372 };
234              
235             local $Template::Stash::LIST_OPS->{fixed_width} = sub {
236 2     2   7 my ( $list, $value ) = @_;
237 2         4 foreach my $item ( @{ $list } ) {
  2         6  
238 10 50       32 my $code = $item->can( 'fixed_width' )
239             or next;
240 10         23 $code->( $item, $value );
241             }
242 2         12 return;
243 61         325 };
244              
245             local $Template::Stash::LIST_OPS->{first_tle} = sub {
246 29     29   83 my ( $list ) = @_;
247 29         73 foreach my $item ( @{ $list } ) {
  29         77  
248 29 50       111 embodies( $item->body(), 'Astro::Coord::ECI::TLE' )
249             and return $item;
250             }
251 0         0 return;
252 61         335 };
253              
254 61         122 local $Template::Stash::LIST_OPS->{to_json} = do {
255 61         130 local $@ = undef;
256             eval {
257 61         394 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         681 } || sub {
265 0     0   0 $self->wail( 'Module JSON could not be loaded' );
266 61 50       157 };
267             };
268              
269             $data{localize} = sub {
270 28     28   63444 return _localize( $tplt_name, @_ );
271 61         343 };
272              
273             # NOTE - must come after $data{localize} because
274             # $data{format_detail} uses $data{localize}
275             $data{format_detail} = sub {
276 173     173   9362 my ( $kind, $evt ) = @_;
277              
278 173 50       953 instance( $evt, FORMAT_VALUE )
279             or return;
280 173 50       837 defined ( my $type = $evt->$kind( width => '' ) )
281             or return;
282 173         788 $type =~ s/ \s+ \z //smx;
283              
284 173         598 foreach my $name ( "$kind:$type", $kind ) {
285 329 100       1414 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         1151 );
292              
293 173         636 chomp $output;
294 173         1399 return $output;
295             }
296 0         0 return __localize(
297             text => [ '+template', "$tplt:$kind" ],
298             );
299 61         544 };
300              
301 61         477 my $output = $self->_process( $tplt, %data );
302              
303             # TODO would love to use \h here, but that needs 5.10.
304 60         1502 $output =~ s/ [ \t]+ (?= \n ) //sxmg;
305             $data{title}->title_gravity() eq $data{TITLE_GRAVITY_BOTTOM}
306 60 100       383 and $output =~ s/ \A \n //smx;
307              
308 60         2140 return $output;
309             }
310              
311             sub gmt {
312 57     57 1 144 my ( $self, @args ) = @_;
313 57 100       151 if ( @args ) {
314 3         10 $self->time_formatter()->gmt( @args );
315 3         17 return $self->SUPER::gmt( @args );
316             } else {
317 54         325 return $self->SUPER::gmt();
318             }
319             }
320              
321             sub local_coord {
322 207     207 1 592 my ( $self, @args ) = @_;
323 207 100       581 if ( @args ) {
324 12         34 my $val = $args[0];
325 12 100       83 defined $val
326             or $val = $self->DEFAULT_LOCAL_COORD;
327              
328 12 50       47 defined $self->template( $val )
329             or $self->wail(
330             'Unknown local coordinate specification', $val );
331              
332 12         79 return $self->SUPER::local_coord( @args );
333             } else {
334 195         1336 return $self->SUPER::local_coord();
335             }
336             }
337              
338             sub permissive {
339 10     10 1 599 my ( $self, @args ) = @_;
340 10 100       47 if ( @args ) {
341 1 50 25     12 if ( $self->{permissive} xor $args[0] ) {
342 1         5 $self->_new_tt( $args[0] );
343             }
344 1         3 $self->{permissive} = $args[0];
345 1         3 return $self;
346             } else {
347 9         49 return $self->{permissive};
348             }
349             }
350              
351             sub template {
352 620     620 1 1561 my ( $self, $name, @value ) = @_;
353 620 50       1435 defined $name
354             or $self->wail( 'Template name not specified' );
355              
356 620 100       1643 if ( @value ) {
357 3         4 my $tplt_text;
358 3 50 33     24 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         13 $self->{canned_template}{$name} = $value[0];
368             }
369              
370 3         12 return $self;
371             } else {
372             defined $self->{canned_template}{$name}
373 617 100       2221 and return $self->{canned_template}{$name};
374 613         2756 return __localize(
375             text => [ '+template', $name ],
376             );
377             }
378             }
379              
380             sub tz {
381 16     16 1 47 my ( $self, @args ) = @_;
382 16 50       47 if ( @args ) {
383 16         60 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         110 $tf->tz( @args );
390 16         40 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   11 my ( $self, $data ) = @_;
398 3 50       20 ARRAY_REF eq ref $data or return;
399              
400 3         7 my @events;
401 3         11 foreach my $pass ( @{ $data } ) {
  3         21  
402 3         18 push @events, $pass->__raw_events();
403             }
404 3 50       13 @events or return;
405 3         25 @events = sort { $a->{time} <=> $b->{time} } @events;
  16         38  
406              
407 3         34 return [ map { $self->_wrap( data => $_ ) } @events ];
  13         37  
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   69 use constant REPORT_CALLER => __PACKAGE__ . '::format';
  8         33  
  8         7247  
416             sub _is_format {
417 62     62   174 my $level = 2; # Start with caller's caller.
418 62         667 while ( my @info = caller( $level ) ) {
419 343 100       723 REPORT_CALLER eq $info[3]
420             and return $level;
421 342         2110 $level++;
422             }
423 61         203 return;
424             }
425              
426             sub _localize {
427 28     28   84 my ( $report, $source, $default ) = @_;
428 28 50       98 defined $default
429             or $default = $source;
430 28 0       72 defined $report
    50          
431             or return defined $source ? $source : $default;
432              
433 28         154 return scalar __localize(
434             text => [ "-$report", 'string', $source ],
435             default => $source,
436             );
437             }
438              
439             sub _process {
440 449     449   2478 my ( $self, $tplt, %arg ) = @_;
441             ARRAY_REF eq ref $arg{arg}
442             and $arg{arg} = Astro::App::Satpass2::Wrap::Array->new(
443 449 100       2859 $arg{arg} );
444 449         871 my $output;
445 449         1136 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       1343 unless ( ref $tplt ) {
455 275         990 while ( defined( my $tplt_text = $self->template( $tplt ) ) ) {
456 272 50       1152 if ( $tplt_text =~ m/\A %% \s* include \s+ ( \w+ ) \s* \z /smxi ) {
457 0         0 $tplt = $1;
458             } else {
459 272         595 $tplt = \$tplt_text;
460 272         786 last;
461             }
462             }
463             }
464              
465 449 100       2571 $tt->process( $tplt, \%arg, \$output )
466             or $self->wail( $tt->error() );
467 448         119727 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   50 my %found;
475 25         65 return ( grep { ! $found{$_}++ } @_ );
  727         1720  
476             }
477              
478             sub _wrap {
479 517     517   2319 my ( $self, %arg ) = @_;
480              
481 517         1203 my $data = $arg{data};
482 517         1265 my $default = $arg{default};
483 517         1037 my $report = $arg{report};
484              
485 517         1182 my $title = ! $data;
486 517   100     2204 $data ||= {};
487 517   66     1767 $default ||= $self->__default();
488              
489 517 50 66     2206 if ( instance( $data, FORMAT_VALUE ) ) {
    100          
    100          
    50          
490             # Do nothing
491             } elsif ( ! defined $data || HASH_REF eq ref $data ) {
492 449         1835 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   546 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         981 );
516             },
517             list_formatter => sub {
518 20     20   51 my ( $data, @arg ) = @_;
519 20         94 my $body = $data->body();
520 20 100       417 my $list_type = $body ? $body->__list_type() : 'inertial';
521 20         175 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         2045 report => $report,
531             title => $title,
532             warner => $self->warner(),
533             );
534 449         1360 $data->add_formatter_method( values %{ $self->{formatter_method} } );
  449         2717  
535             } elsif ( ARRAY_REF eq ref $data ) {
536 45         113 $data = [ map { $self->_wrap( data => $_, report => $report ) } @{ $data } ];
  85         303  
  45         135  
537             } elsif ( embodies( $data, 'Astro::Coord::ECI' ) ) {
538 23         816 $data = $self->_wrap(
539             data => { body => $data },
540             report => $report,
541             );
542             }
543              
544 517         2942 return $data;
545             }
546              
547             __PACKAGE__->create_attribute_methods();
548              
549             1;
550              
551             __END__