File Coverage

blib/lib/RF/Antenna/Planet/MSI/Format.pm
Criterion Covered Total %
statement 238 256 92.9
branch 115 152 75.6
condition 3 3 100.0
subroutine 34 36 94.4
pod 26 26 100.0
total 416 473 87.9


line stmt bran cond sub pod time code
1             package RF::Antenna::Planet::MSI::Format;
2 13     13   1292080 use strict;
  13         184  
  13         436  
3 13     13   74 use warnings;
  13         26  
  13         342  
4 13     13   67 use Scalar::Util qw();
  13         38  
  13         312  
5 13     13   6422 use Tie::IxHash qw{};
  13         44807  
  13         326  
6 13     13   1751 use Path::Class qw{};
  13         171177  
  13         388  
7 13     13   5818 use RF::Functions 0.04, qw{dbd_dbi dbi_dbd};
  13         124247  
  13         45876  
8              
9             our $VERSION = '0.14';
10             our $PACKAGE = __PACKAGE__;
11              
12             =head1 NAME
13              
14             RF::Antenna::Planet::MSI::Format - RF Antenna Pattern File Reader and Writer in Planet MSI Format
15              
16             =head1 SYNOPSIS
17              
18             Read from MSI file
19              
20             use RF::Antenna::Planet::MSI::Format;
21             my $antenna = RF::Antenna::Planet::MSI::Format->new;
22             $antenna->read($filename);
23              
24             Create a blank object, load data from other sources, then write antenna pattern file.
25              
26             my $antenna = RF::Antenna::Planet::MSI::Format->new;
27             $antenna->name("My Name");
28             $antenna->make("My Make");
29             my $file = $antenna->write($filename);
30              
31             =head1 DESCRIPTION
32              
33             This package reads and writes antenna radiation patterns in Planet MSI antenna format.
34              
35             Planet is a RF propagation simulation tool initially developed by MSI. Planet was a 2G radio planning tool which has set a standard in the early days of computer aided radio network design. The antenna pattern file and the format which is currently known as the ".msi" format or an msi file has become a standard.
36              
37             =head1 CONSTRUCTORS
38              
39             =head2 new
40              
41             Creates a new blank object for creating files or loading data from other sources
42              
43             my $antenna = RF::Antenna::Planet::MSI::Format->new;
44              
45             Creates a new object and loads data from other sources
46              
47             my $antenna = RF::Antenna::Planet::MSI::Format->new(
48             NAME => "My Antenna Name",
49             MAKE => "My Manufacturer Name",
50             FREQUENCY => "2437" || "2437 MHz" || "2.437 GHz",
51             GAIN => "10.0" || "10.0 dBd" || "12.15 dBi",
52             COMMENT => "My Comment",
53             horizontal => [[0.00, 0.96], [1.00, 0.04], ..., [180.00, 31.10], ..., [359.00, 0.04]],
54             vertical => [[0.00, 1.08], [1.00, 0.18], ..., [180.00, 31.23], ..., [359.00, 0.18]],
55             );
56              
57             =cut
58              
59             sub new {
60 21     21 1 6655 my $this = shift;
61 21 50       113 die('Error: new constructor requires key/value pairs') if @_ % 2;
62 21 50       78 my $class = ref($this) ? ref($this) : $this;
63 21         47 my $self = {};
64 21         54 bless $self, $class;
65              
66 21         54 my @later = ();
67 21         73 while (@_) { #preserve order
68 4         10 my $key = shift;
69 4         6 my $value = shift;
70 4 100       17 if ($key eq 'header') {
    50          
    50          
71 2 100       8 if (ref($value) eq 'HASH') {
    50          
72 1         6 my @order = qw{NAME MAKE FREQUENCY GAIN TILT POLARIZATION COMMENT};
73 1         7 my %copy = %$value;
74 1         2 foreach my $key (@order) {
75 7 100       22 $self->header($key => delete($copy{$key})) if exists $copy{$key};
76             }
77 1         6 $self->header(%copy); #appends the rest in hash order
78             } elsif (ref($value) eq 'ARRAY') {
79 1         4 $self->header(@$value); #preserves order
80             } else {
81 0         0 die('Error: header value expected to be either array reference or hash reference');
82             }
83             } elsif ($key eq 'horizontal') {
84 0 0       0 die('Error: horizontal value expected to be array reference') unless ref($value) eq 'ARRAY';
85 0         0 $self->horizontal($value);
86             } elsif ($key eq 'vertical') {
87 0 0       0 die('Error: vertical value expected to be array reference') unless ref($value) eq 'ARRAY';
88 0         0 $self->vertical($value);
89             } else {
90 2 50       6 die('Error: header key/value pairs must be strings') if ref($value);
91 2         6 push @later, $key, $value; #store for later so that we process header before header keys
92             }
93             }
94 21 100       64 $self->header(@later) if @later;
95              
96 21         67 return $self;
97             }
98              
99             =head2 read
100              
101             Reads an antenna pattern file and parses the data into the object data structure. Returns the object so that the call can be chained.
102              
103             $antenna->read($filename);
104             $antenna->read(\$scalar);
105              
106             Assumptions:
107             The first line in the MSI file contains the name of the antenna. It appears that some vendors suppress the "NAME" token but we always write the NAME token.
108             The keys can be mixed case but convention appears to be all upper case keys for common keys and lower case keys for vendor extensions.
109              
110             =cut
111              
112 0         0 sub read {
113 9     9 1 5196 my $self = shift;
114 9         21 my $file = shift;
115 9 100       47 my $blob = ref($file) eq 'SCALAR' ? ${$file} : Path::Class::file($file)->slurp;
  7         16  
116 9 100       970 die(qq{Error: Package: $PACKAGE: Method: read, file: "$file" is empty}) unless length($blob);
117 8         49 $self->{'blob'} = $blob; #store for blob method
118 8         765 my @lines = split(/[\n\r]+/, $blob);
119 8         94 my $loop = 0;
120 8         30 while (1) {
121 34         69 my $line = shift @lines;
122 34         127 $line =~ s/\A\s*//; #ltrim
123 34         176 $line =~ s/\s*\Z//; #rtrim
124 34 100       92 if ($line) {
125 30         45 $loop++;
126 30         114 my ($key, $value) = split /\s+/, $line, 2; #split with limit returns undef value if empty string
127 30 100       89 $value = '' unless defined $value;
128            
129 30 100 100     119 if ($loop == 1 and $key ne 'NAME') { #First line of file is NAME even if NAME token is surpessed
130 2         7 $self->header(NAME => $line);
131             } else {
132             #printf "Key: $key, Value: $value\n";
133 28 100       98 if (uc($key) eq 'HORIZONTAL') {
    100          
134 6         23 $self->_parse_polarization(\@lines, horizontal => $value);
135             } elsif (uc($key) eq 'VERTICAL') {
136 6         30 $self->_parse_polarization(\@lines, vertical => $value);
137             } else {
138 16         46 $self->header($key => $value);
139             }
140             }
141             }
142 34 100       89 last unless @lines;
143             }
144 8         48 return $self;
145              
146             sub _parse_polarization {
147 12     12   28 my $self = shift;
148 12         22 my $lines = shift;
149 12         27 my $method = shift;
150 12         25 my $value = shift; #string
151 12 100       51 if ($value =~ m/([0-9]+)/) { #support bad data like missing 360 or "360 0"
152 10         41 $value = $1 + 0; #convert string to number
153             } else {
154 2         9 $value = 360; #default
155             }
156 12         53 my @data = map {s/\s+\Z//; s/\A\s+//; [split /\s+/, $_, 2]} splice @$lines, 0, $value;
  1808         3728  
  1808         3012  
  1808         5949  
157 12 50       138 die(sprintf('Error: %s records with %s records returned %s records', uc($method), $value, scalar(@data))) unless scalar(@data) == $value;
158 12         61 $self->$method(\@data);
159             }
160             }
161              
162             =head2 read_fromZipMember
163              
164             Reads an antenna pattern file from a zipped archive and parses the data into the object data structure.
165              
166             $antenna->read_fromZipMember($zip_filename, $member_filename);
167              
168             =cut
169              
170             sub read_fromZipMember {
171 1     1 1 609 my $self = shift;
172 1 50       5 my $zip_filename = shift or die('Error: zip filename required');
173 1 50       10 my $member_filename = shift or die('Error: zip member name requried');
174              
175 1         767 require Archive::Zip;
176 1         63831 my $zip_archive = Archive::Zip->new;
177 1 50       46 unless ( $zip_archive->read("$zip_filename") == Archive::Zip::AZ_OK() ) {die qq{Error: zip file "$zip_filename" read error}};
  0         0  
178 1 50       3129 my $member = $zip_archive->memberNamed($member_filename) or die(qq{Error: zip file "$zip_filename" could not find member "$member_filename"});
179 1         74 my $blob = $member->contents;
180 1         1281 return $self->read(\$blob);
181             }
182              
183             =head2 blob
184              
185             Returns the data blob that was read by the read($file), read($scalar_ref), or read_fromZipMember($,$) methods.
186              
187             =cut
188              
189             sub blob {
190 1     1 1 718 my $self = shift;
191 1         3 return $self->{'blob'};
192             }
193              
194             =head2 write
195              
196             Writes the object's data to an antenna pattern file and returns a Path::Class file object of the written file.
197              
198             my $file = $antenna->write($filename); #isa Path::Class::file
199             my $tempfile = $antenna->write; #isa Path::Class::file in temp directory
200             $antenna->write(\$scalar); #returns undef with data writen to the variable
201              
202             =cut
203              
204             sub write {
205 2     2 1 513 my $self = shift;
206 2         7 my $filename = shift;
207              
208             #Open file handle
209 2         6 my $fh;
210             my $file;
211 2 50       12 if (ref($filename) eq 'SCALAR') {
    0          
212 2         5 $file = undef;
213 2     2   122 open $fh, '>', $filename;
  2         18  
  2         4  
  2         15  
214             } elsif (length $filename) {
215 0         0 $file = Path::Class::file($filename);
216 0 0       0 $fh = $file->open('w') or die(qq{Error: Cannot open "$filename" for writing});
217             } else {
218 0         0 require File::Temp;
219 0         0 my $suffix = $self->file_extension;
220 0         0 ($fh, $filename) = File::Temp::tempfile('antenna_pattern_XXXXXXXX', TMPDIR => 1, SUFFIX => $suffix);
221 0         0 $file = Path::Class::file($filename);
222             }
223              
224             #Print to file handle
225              
226             sub _print_fh_key_value {
227 736     736   1013 my $fh = shift;
228 736         990 my $key = shift;
229 736         972 my $value = shift;
230 736         2129 print $fh "$key $value\n";
231             }
232              
233 2         1894 my $header = $self->header; #isa Tie::IxHash ordered hash
234              
235             ##Print NAME as first line
236 2         15 _print_fh_key_value($fh, 'NAME', $header->{'NAME'});
237              
238             ##Print rest of the headers
239 2         14 foreach my $key (keys %$header) {
240 10 100       102 next if $key eq 'NAME'; #written above
241 8         25 my $value = $header->{$key};
242 8 50       71 _print_fh_key_value($fh, $key, $value) if defined $value;
243             }
244              
245             sub _print_fh_key_array {
246 4     4   9 my $fh = shift;
247 4         8 my $key = shift;
248 4         7 my $array = shift;
249 4 50       15 if (@$array) {
250 4         35 _print_fh_key_value($fh, $key, scalar(@$array));
251 4         25 foreach my $row (@$array) {
252 722         1188 my $key = $row->[0];
253 722         982 my $value = $row->[1];
254 722         1074 _print_fh_key_value($fh, $key, $value);
255             }
256             }
257             }
258              
259             ##Print antenna pattern angle and loss values
260 2         10 foreach my $method (qw{horizontal vertical}) {
261 4         21 my $array = $self->$method;
262 4 50       11 next unless $array;
263 4         13 my $key = uc($method);
264 4         13 _print_fh_key_array($fh, $key, $array);
265             }
266              
267             #Close file handle and Return file object
268 2         21 close $fh;
269 2         11 return $file;
270             }
271              
272             =head2 file_extension
273              
274             Sets and returns the file extension to use for write method when called without any parameters.
275            
276             my $suffix = $antenna->file_extension('.ant');
277              
278             Default: .msi
279              
280             Alternatives: .pla, .pln, .ptn, .txt, .ant
281              
282             =cut
283              
284             sub file_extension {
285 0     0 1 0 my $self = shift;
286 0 0       0 $self->{'file_extension'} = shift if @_;
287 0 0       0 $self->{'file_extension'} = '.msi' unless defined($self->{'write_file_extension'});
288 0         0 return $self->{'file_extension'};
289             }
290              
291             =head2 media_type
292              
293             Returns the Media Type (formerly known as MIME Type) for use in Internet applications.
294              
295             Default: application/vnd.planet-antenna-pattern
296              
297             =cut
298              
299 0     0 1 0 sub media_type {'application/vnd.planet-antenna-pattern'};
300              
301             =head1 DATA STRUCTURE METHODS
302              
303             =head2 header
304              
305             Set header values and returns the header data structure which is a hash reference tied to L to preserve header sort order.
306              
307             Set a key/value pair
308              
309             $antenna->header(COMMENT => "My comment"); #upper case keys are common/reserved whereas mixed/lower case keys are vendor extensions
310              
311             Set multiple keys/values with one call
312              
313             $antenna->header(NAME => $myname, MAKE => $mymake);
314              
315             Read arbitrary values
316              
317             my $value = $antenna->header->{$key};
318              
319             Returns ordered list of header keys
320              
321             my @keys = keys %{$antenna->header};
322              
323             Common Header Keys: NAME MAKE FREQUENCY GAIN TILT POLARIZATION COMMENT
324              
325             =cut
326              
327             sub header {
328 325     325 1 1071 my $self = shift;
329 325 50       744 die('Error: header method requires key/value pairs') if @_ % 2;
330 325 100       774 unless (defined $self->{'header'}) {
331 19         43 my %data = ();
332 19         134 tie(%data, 'Tie::IxHash');
333 19         399 $self->{'header'} = \%data;
334             }
335 325         727 while (@_) {
336 86         194 my $key = shift;
337 86         142 my $value = shift;
338 86         418 $self->{'header'}->{$key} = $value;
339             }
340 325         2385 return $self->{'header'};
341             }
342              
343             =head2 horizontal
344              
345             Sets and returns the horizontal data structure for angles with relative loss values from the specified gain in the header. The data structure is an array reference of array references [[$angle1, $value1], [$angle2, $value2], ...]
346              
347             Conventions: The industry has standardized on using 360 points from 0 to 359 degrees with non-negative loss values. The angle 0 is the boresight with increasing values continuing clockwise (e.g., top-down view). Typically, plots show horizontal patterns with 0 degrees pointing up (i.e., North). This is standard compass convention.
348              
349             =cut
350              
351             sub horizontal {
352 1103     1103 1 367459 my $self = shift;
353 1103 100       2989 $self->{'horizontal'} = shift if @_;
354 1103         5448 return $self->{'horizontal'};
355             }
356              
357             =head2 vertical
358              
359             Sets and returns the vertical data structure for angles with relative loss values from the specified gain in the header. The data structure is an array reference of array references [[$angle1, $value1], [$angle2, $value2], ...]
360              
361             Conventions: The industry has standardized on using 360 points from 0 to 359 degrees with non-negative loss values. The angle 0 is the boresight with increasing values continuing clockwise (e.g., left-side view). The angle 0 is the boresight pointing towards the horizon with increasing values continuing clockwise where 90 degrees is pointing to the ground and 270 is pointing into the sky. Typically, plots show vertical patterns with 0 degrees pointing right (i.e., East).
362              
363             =cut
364              
365             sub vertical {
366 1103     1103 1 2645 my $self = shift;
367 1103 100       2860 $self->{'vertical'} = shift if @_;
368 1103         5046 return $self->{'vertical'};
369             }
370              
371             =head1 HELPER METHODS
372              
373             Helper methods are wrappers around the header data structure to aid in usability.
374              
375             =head2 name
376              
377             Sets and returns the name of the antenna in the header structure
378              
379             my $name = $antenna->name;
380             $antenna->name("My Antenna Name");
381              
382             Assumed: Less than about 40 ASCII characters
383              
384             =cut
385              
386             sub name {
387 12     12 1 574 my $self = shift;
388 12 100       55 $self->header(NAME => shift) if @_;
389 12         50 return $self->header->{'NAME'};
390             }
391              
392             =head2 make
393              
394             Sets and returns the name of the manufacturer in the header structure
395              
396             my $make = $antenna->make;
397             $antenna->make("My Antenna Manufacturer");
398              
399             Assumed: Less than about 40 ASCII characters
400              
401             =cut
402              
403             sub make {
404 3     3 1 1794 my $self = shift;
405 3 50       12 $self->header(MAKE => shift) if @_;
406 3         7 return $self->header->{'MAKE'};
407             }
408              
409             =head2 frequency
410              
411             Sets and returns the frequency string as displayed in header structure
412              
413             my $frequency = $antenna->frequency;
414             $antenna->frequency("2450"); #correct format in MHz
415             $antenna->frequency("2450 MHz"); #acceptable format
416             $antenna->frequency("2.45 GHz"); #common format but technically not to spec
417             $antenna->frequency("2450-2550"); #common range format but technically not to spec
418             $antenna->frequency("2.45-2.55 GHz"); #common range format but technically not to spec
419              
420             =cut
421              
422             sub frequency {
423 53     53 1 3419 my $self = shift;
424 53 100       154 $self->header(FREQUENCY => shift) if @_;
425 53         110 return $self->header->{'FREQUENCY'};
426             }
427              
428             =head2 frequency_mhz, frequency_ghz, frequency_mhz_lower, frequency_mhz_upper, frequency_ghz_lower, frequency_ghz_upper
429              
430             Attempts to read and parse the string header value and return the frequency as a number in the requested unit of measure.
431              
432             =cut
433              
434             #supported formats
435             #123.1 => assumed MHz
436             #123.1 MHz
437             #123.1 GHz
438             #123.1 kHz
439             #123.1-124.1 => assumed MHz
440             #123.1-124.1 MHz
441             #123.1-124.1 GHz
442             #123.1-124.1 kHz
443             #123x124
444             #123x124 MHz
445             #123x124 GHz
446             #123x124 kHz
447              
448             sub frequency_mhz {
449 35     35 1 6568 my $self = shift;
450 35         110 my $string = $self->frequency;
451 35         276 my $number = undef; #return undef if cannot parse
452              
453 35 50       86 if (defined($string)) {
454              
455 35         58 my $upper = undef;
456 35         55 my $lower = undef;
457              
458 35         54 my $scale = 1; #Default: MHz
459 35 100       191 if ($string =~ m/GHz/i) {
    100          
    100          
460 14         25 $scale = 1e3;
461             } elsif ($string =~ m/kHz/i) {
462 5         13 $scale = 1e-3;
463             } elsif ($string =~ m/MHz/i) {
464 2         4 $scale = 1;
465             }
466              
467 35 100       366 if (Scalar::Util::looks_like_number($string)) { #entire string looks like a number
    100          
    50          
468 11         31 $number = $scale * $string;
469 11         20 $lower = $number;
470 11         20 $upper = $number;
471             } elsif ($string =~ m/([0-9]*\.?[0-9]+)[^0-9.]+([0-9]*\.?[0-9]+)/) { #two non-negative numbers with any separator
472 18         60 $lower = $scale * $1;
473 18         33 $upper = $scale * $2;
474 18         40 $number = ($lower + $upper) / 2;
475             } elsif ($string =~ m/([0-9]*\.?[0-9]+)/) { #one non-negative number
476 6         23 $number = $scale * $1;
477 6         10 $lower = $number;
478 6         10 $upper = $number;
479             }
480 35         70 $self->{'frequency_mhz'} = $number;
481 35         64 $self->{'frequency_mhz_lower'} = $lower;
482 35         61 $self->{'frequency_mhz_upper'} = $upper;
483              
484             }
485 35         95 return $number;
486             }
487              
488             sub frequency_ghz {
489 9     9 1 24 my $self = shift;
490 9         24 my $mhz = $self->frequency_mhz;
491 9 50       62 return $mhz ? $mhz/1000 : undef;
492             }
493              
494             sub frequency_mhz_lower {
495 7     7 1 15 my $self = shift;
496 7         19 $self->frequency_mhz; #initialize
497 7         26 return $self->{'frequency_mhz_lower'};
498             }
499              
500             sub frequency_mhz_upper {
501 7     7 1 15 my $self = shift;
502 7         21 $self->frequency_mhz; #initialize
503 7         27 return $self->{'frequency_mhz_upper'};
504             }
505              
506             sub frequency_ghz_lower {
507 2     2 1 5 my $self = shift;
508 2         5 my $mhz = $self->frequency_mhz_lower;
509 2 50       12 return $mhz ? $mhz/1000 : undef;
510             }
511              
512             sub frequency_ghz_upper {
513 2     2 1 6 my $self = shift;
514 2         5 my $mhz = $self->frequency_mhz_upper;
515 2 50       10 return $mhz ? $mhz/1000 : undef;
516             }
517              
518             =head2 gain
519              
520             Sets and returns the antenna gain string as displayed in file (dBd is the default unit of measure)
521              
522             my $gain = $antenna->gain;
523             $antenna->gain("9.1"); #correct format in dBd
524             $antenna->gain("9.1 dBd"); #correct format in dBd
525             $antenna->gain("9.1 dBi"); #correct format in dBi
526             $antenna->gain("(dBi) 9.1"); #supported format
527              
528             =cut
529              
530             sub gain {
531 74     74 1 10587 my $self = shift;
532 74 100       224 $self->header(GAIN => shift) if @_;
533 74         193 return $self->header->{'GAIN'};
534             }
535              
536             =head2 gain_dbd, gain_dbi
537              
538             Attempts to read and parse the string header value and return the gain as a number in the requested unit of measure.
539              
540             =cut
541              
542             sub gain_dbd {
543 48     48 1 12465 my $self = shift;
544 48         109 my $string = $self->gain;
545 48         408 my $number = undef;
546 48 50       144 if (defined($string)) {
547              
548 48 100       349 if (Scalar::Util::looks_like_number($string)) { #entire string looks like a number
    50          
549 12         32 $number = $string + 0; #default: dBd
550             } elsif ($string =~ m/([+-]?[0-9]*\.?[0-9]+)/) { #extract number
551 36         97 my $match = $1;
552 36 100       166 $number = $string =~ m/dBi/i ? dbd_dbi($match) : $match + 0;
553             }
554              
555             }
556 48         263 return $number;
557             }
558              
559             sub gain_dbi {
560 24     24 1 55 my $self = shift;
561 24         52 my $dbd = $self->gain_dbd;
562 24 50       86 return defined($dbd) ? dbi_dbd($dbd) : undef;
563             }
564              
565             =head2 tilt
566              
567             Antenna tilt string as displayed in file.
568              
569             my $tilt = $antenna->tilt;
570             $antenna->tilt("MECHANICAL");
571             $antenna->tilt("ELECTRICAL");
572              
573             =cut
574              
575             sub tilt {
576 47     47 1 1059 my $self = shift;
577 47 100       148 $self->header(TILT => shift) if @_;
578 47         101 return $self->header->{'TILT'};
579             }
580              
581              
582             =head2 electrical_tilt
583              
584             Antenna electrical_tilt string as displayed in file.
585              
586             my $electrical_tilt = $antenna->electrical_tilt;
587             $antenna->electrical_tilt("4"); #4-degree downtilt
588              
589             =cut
590              
591             sub electrical_tilt {
592 18     18 1 1062 my $self = shift;
593 18 100       56 $self->header(ELECTRICAL_TILT => shift) if @_;
594 18         37 return $self->header->{'ELECTRICAL_TILT'};
595             }
596              
597             =head2 electrical_tilt_degrees
598              
599             Attempts to read and parse the header and return the electrical down tilt in degrees.
600              
601             my $degrees = $antenna->electrical_tilt_degrees; #isa number
602              
603             Note: I recommend storing electrical downtilt in the TILT and ELECTRICAL_TILT headers like this:
604              
605             TILT ELECTRICAL
606             ELECTRICAL_TILT 4
607              
608             However, this method attempts to read as many different formats as found in the source files.
609              
610             =cut
611              
612             sub electrical_tilt_degrees {
613 28     28 1 14885 my $self = shift;
614 28         49 my $degrees = undef;
615 28         57 my $tilt = $self->tilt;
616 28 100       231 if (defined $tilt) {
617 27         92 $tilt =~ s/\A\s+//; #ltrim
618 27         76 $tilt =~ s/\s+\Z//; #rtrim
619 27 100       261 if ($tilt =~ m/\A(NONE|MECHANICAL)/i ) { #Spec:
    100          
    100          
    100          
    100          
    100          
    100          
    100          
620 3         7 $degrees = 0;
621             } elsif (Scalar::Util::looks_like_number($tilt) ) { #number (assume electrical tilt)
622 1         7 $degrees = abs($tilt + 0);
623             } elsif ($tilt =~ m/\A-?([0-9]{1,2})[-\s]deg.*ELECTRICAL/i) { #8-Deg Electrical
624 1         4 $degrees = $1 + 0;
625             } elsif ($tilt =~ m/\A-?([0-9]{1,2})[-\s]deg.*E-TILT/i ) { #8-Deg E-Tilt
626 1         10 $degrees = $1 + 0;
627             } elsif ($tilt =~ m/\A([0-9]{1,2})T\Z/i ) { #11T
628 2         6 $degrees = $1 + 0;
629             } elsif ($tilt =~ m/\AT([0-9]{1,2})\Z/i ) { #T11
630 2         8 $degrees = $1 + 0;
631             } elsif ($tilt =~ m/\AELECTRICAL -?([0-9]{1,2})\b/ ) { #ELECTRICAL 11...
632 2         7 $degrees = $1 + 0;
633             } elsif ($tilt =~ m/\AELECTRICAL\Z/i ) { #Spec: ELECTRICAL
634 11 100       27 my $comment = $self->comment; $comment = '' unless defined($comment);
  11         85  
635 11 100       22 my $electrical_tilt = $self->electrical_tilt; $electrical_tilt = '' unless defined($electrical_tilt);
  11         85  
636 11         24 $electrical_tilt =~ s/\A\s+//; #ltrim
637 11         18 $electrical_tilt =~ s/\s+\Z//; #rtrim
638 11 100       60 if (Scalar::Util::looks_like_number($electrical_tilt) ) { #Spec: ELECTRICAL_TILT 1.25
    100          
    100          
    100          
639 2         8 $degrees = abs($electrical_tilt + 0);
640             } elsif ($electrical_tilt =~ m/\A-?([0-9]{1,2})\b/i ) { #ELECTRICAL_TILT 11 degrees
641 1         4 $degrees = $1 + 0;
642             } elsif ($comment =~ m/ELECTRICAL_TILT\s+-?([0-9]{1,2})\b/i) { #COMMENT ELECTRICAL_TILT 8 | COMMENT ELECTRICAL_TILT 8 degrees
643 2         7 $degrees = $1 + 0;
644             } elsif ($comment =~ m/E-?TILT\s+-?([0-9]{1,2})\b/i ) { #COMMENT E-TILT 8 | COMMENT ETilt -2 deg
645 3         12 $degrees = $1 + 0;
646             }
647             }
648             }
649 28         206 return $degrees;
650             }
651              
652             =head2 comment
653              
654             Antenna comment string as displayed in file.
655              
656             my $comment = $antenna->comment;
657             $antenna->comment("My Comment");
658              
659             =cut
660              
661             sub comment {
662 20     20 1 2116 my $self = shift;
663 20 100       70 $self->header(COMMENT => shift) if @_;
664 20         42 return $self->header->{'COMMENT'};
665             }
666              
667             =head1 SEE ALSO
668              
669             Format Definition: L
670              
671             Antenna Pattern File Library L
672              
673             Format Definition from RCC: L
674              
675             =head1 AUTHOR
676              
677             Michael R. Davis
678              
679             =head1 COPYRIGHT AND LICENSE
680              
681             MIT License
682              
683             Copyright (c) 2022 Michael R. Davis
684              
685             =cut
686              
687             1;