File Coverage

blib/lib/RF/Antenna/Planet/MSI/Format.pm
Criterion Covered Total %
statement 236 254 92.9
branch 111 148 75.0
condition 7 7 100.0
subroutine 34 36 94.4
pod 26 26 100.0
total 414 471 87.9


line stmt bran cond sub pod time code
1             package RF::Antenna::Planet::MSI::Format;
2 13     13   1295978 use strict;
  13         166  
  13         390  
3 13     13   73 use warnings;
  13         28  
  13         355  
4 13     13   67 use Scalar::Util qw();
  13         27  
  13         290  
5 13     13   6405 use Tie::IxHash qw{};
  13         44915  
  13         327  
6 13     13   1736 use Path::Class qw{};
  13         160602  
  13         355  
7 13     13   5649 use RF::Functions 0.04, qw{dbd_dbi dbi_dbd};
  13         121701  
  13         44499  
8              
9             our $VERSION = '0.13';
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 7328 my $this = shift;
61 21 50       107 die('Error: new constructor requires key/value pairs') if @_ % 2;
62 21 50       86 my $class = ref($this) ? ref($this) : $this;
63 21         53 my $self = {};
64 21         53 bless $self, $class;
65              
66 21         53 my @later = ();
67 21         83 while (@_) { #preserve order
68 4         6 my $key = shift;
69 4         8 my $value = shift;
70 4 100       16 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         5 my %copy = %$value;
74 1         4 foreach my $key (@order) {
75 7 100       19 $self->header($key => delete($copy{$key})) if exists $copy{$key};
76             }
77 1         4 $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       68 $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 5570 my $self = shift;
114 9         21 my $file = shift;
115 9 100       50 my $blob = ref($file) eq 'SCALAR' ? ${$file} : Path::Class::file($file)->slurp;
  7         38  
116 9 100       1033 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         779 my @lines = split(/[\n\r]+/, $blob);
119 8         88 my $loop = 0;
120 8         36 while (1) {
121 34         72 my $line = shift @lines;
122 34         127 $line =~ s/\A\s*//; #ltrim
123 34         168 $line =~ s/\s*\Z//; #rtrim
124 34 100       90 if ($line) {
125 30         46 $loop++;
126 30         126 my ($key, $value) = split /\s+/, $line, 2; #split with limit returns undef value if empty string
127 30 100       87 $value = '' unless defined $value;
128            
129 30 100 100     125 if ($loop == 1 and $key ne 'NAME') { #First line of file is NAME even if NAME token is surpessed
130 2         10 $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         24 $self->_parse_polarization(\@lines, horizontal => $value);
135             } elsif (uc($key) eq 'VERTICAL') {
136 6         25 $self->_parse_polarization(\@lines, vertical => $value);
137             } else {
138 16         52 $self->header($key => $value);
139             }
140             }
141             }
142 34 100       97 last unless @lines;
143             }
144 8         47 return $self;
145              
146             sub _parse_polarization {
147 12     12   26 my $self = shift;
148 12         19 my $lines = shift;
149 12         25 my $method = shift;
150 12         25 my $value = shift; #string
151 12 100       47 if ($value =~ m/([0-9]+)/) { #support bad data like missing 360 or "360 0"
152 10         39 $value = $1 + 0; #convert string to number
153             } else {
154 2         4 $value = 360; #default
155             }
156 12         55 my @data = map {s/\s+\Z//; s/\A\s+//; [split /\s+/, $_, 2]} splice @$lines, 0, $value;
  1808         3842  
  1808         3001  
  1808         5727  
157 12 50       141 die(sprintf('Error: %s records with %s records returned %s records', uc($method), $value, scalar(@data))) unless scalar(@data) == $value;
158 12         65 $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 876 my $self = shift;
172 1 50       4 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         747 require Archive::Zip;
176 1         66365 my $zip_archive = Archive::Zip->new;
177 1 50       54 unless ( $zip_archive->read("$zip_filename") == Archive::Zip::AZ_OK() ) {die qq{Error: zip file "$zip_filename" read error}};
  0         0  
178 1 50       3205 my $member = $zip_archive->memberNamed($member_filename) or die(qq{Error: zip file "$zip_filename" could not find member "$member_filename"});
179 1         78 my $blob = $member->contents;
180 1         1312 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 1140 my $self = shift;
191 1         5 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 512 my $self = shift;
206 2         6 my $filename = shift;
207              
208             #Open file handle
209 2         5 my $fh;
210             my $file;
211 2 50       14 if (ref($filename) eq 'SCALAR') {
    0          
212 2         6 $file = undef;
213 2     2   108 open $fh, '>', $filename;
  2         17  
  2         6  
  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   1019 my $fh = shift;
228 736         1012 my $key = shift;
229 736         1016 my $value = shift;
230 736         1956 print $fh "$key $value\n";
231             }
232              
233 2         1820 my $header = $self->header; #isa Tie::IxHash ordered hash
234              
235             ##Print NAME as first line
236 2         14 _print_fh_key_value($fh, 'NAME', $header->{'NAME'});
237              
238             ##Print rest of the headers
239 2         17 foreach my $key (keys %$header) {
240 10 100       139 next if $key eq 'NAME'; #written above
241 8         29 my $value = $header->{$key};
242 8 50       76 _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         6 my $key = shift;
248 4         9 my $array = shift;
249 4 50       15 if (@$array) {
250 4         38 _print_fh_key_value($fh, $key, scalar(@$array));
251 4         27 foreach my $row (@$array) {
252 722         1185 my $key = $row->[0];
253 722         978 my $value = $row->[1];
254 722         1117 _print_fh_key_value($fh, $key, $value);
255             }
256             }
257             }
258              
259             ##Print antenna pattern angle and loss values
260 2         9 foreach my $method (qw{horizontal vertical}) {
261 4         20 my $array = $self->$method;
262 4 50       18 next unless $array;
263 4         10 my $key = uc($method);
264 4         12 _print_fh_key_array($fh, $key, $array);
265             }
266              
267             #Close file handle and Return file object
268 2         8 close $fh;
269 2         13 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 1096 my $self = shift;
329 325 50       793 die('Error: header method requires key/value pairs') if @_ % 2;
330 325 100       805 unless (defined $self->{'header'}) {
331 19         46 my %data = ();
332 19         145 tie(%data, 'Tie::IxHash');
333 19         423 $self->{'header'} = \%data;
334             }
335 325         744 while (@_) {
336 86         195 my $key = shift;
337 86         145 my $value = shift;
338 86         445 $self->{'header'}->{$key} = $value;
339             }
340 325         2489 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 367626 my $self = shift;
353 1103 100       2898 $self->{'horizontal'} = shift if @_;
354 1103         5169 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 2729 my $self = shift;
367 1103 100       2925 $self->{'vertical'} = shift if @_;
368 1103         5012 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 632 my $self = shift;
388 12 100       59 $self->header(NAME => shift) if @_;
389 12         52 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 1786 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 3668 my $self = shift;
424 53 100       170 $self->header(FREQUENCY => shift) if @_;
425 53         122 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 7653 my $self = shift;
450 35         85 my $string = $self->frequency;
451 35         295 my $number = undef; #return undef if cannot parse
452              
453 35 50       86 if (defined($string)) {
454              
455 35         52 my $upper = undef;
456 35         61 my $lower = undef;
457              
458 35         52 my $scale = 1; #Default: MHz
459 35 100       198 if ($string =~ m/GHz/i) {
    100          
    100          
460 14         32 $scale = 1e3;
461             } elsif ($string =~ m/kHz/i) {
462 5         15 $scale = 1e-3;
463             } elsif ($string =~ m/MHz/i) {
464 2         4 $scale = 1;
465             }
466              
467 35 100       388 if (Scalar::Util::looks_like_number($string)) { #entire string looks like a number
    100          
    50          
468 11         32 $number = $scale * $string;
469 11         21 $lower = $number;
470 11         18 $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         62 $lower = $scale * $1;
473 18         38 $upper = $scale * $2;
474 18         41 $number = ($lower + $upper) / 2;
475             } elsif ($string =~ m/([0-9]*\.?[0-9]+)/) { #one non-negative number
476 6         23 $number = $scale * $1;
477 6         11 $lower = $number;
478 6         11 $upper = $number;
479             }
480 35         65 $self->{'frequency_mhz'} = $number;
481 35         68 $self->{'frequency_mhz_lower'} = $lower;
482 35         65 $self->{'frequency_mhz_upper'} = $upper;
483              
484             }
485 35         101 return $number;
486             }
487              
488             sub frequency_ghz {
489 9     9 1 23 my $self = shift;
490 9         26 my $mhz = $self->frequency_mhz;
491 9 50       63 return $mhz ? $mhz/1000 : undef;
492             }
493              
494             sub frequency_mhz_lower {
495 7     7 1 12 my $self = shift;
496 7         22 $self->frequency_mhz; #initialize
497 7         30 return $self->{'frequency_mhz_lower'};
498             }
499              
500             sub frequency_mhz_upper {
501 7     7 1 14 my $self = shift;
502 7         21 $self->frequency_mhz; #initialize
503 7         29 return $self->{'frequency_mhz_upper'};
504             }
505              
506             sub frequency_ghz_lower {
507 2     2 1 5 my $self = shift;
508 2         7 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 5 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 10769 my $self = shift;
532 74 100       244 $self->header(GAIN => shift) if @_;
533 74         167 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 13012 my $self = shift;
544 48         105 my $string = $self->gain;
545 48         409 my $number = undef;
546 48 50       111 if (defined($string)) {
547              
548 48 100       365 if (Scalar::Util::looks_like_number($string)) { #entire string looks like a number
    50          
549 12         36 $number = $string + 0; #default: dBd
550             } elsif ($string =~ m/([+-]?[0-9]*\.?[0-9]+)/) { #extract number
551 36         96 my $match = $1;
552 36 100       167 $number = $string =~ m/dBi/i ? dbd_dbi($match) : $match + 0;
553             }
554              
555             }
556 48         261 return $number;
557             }
558              
559             sub gain_dbi {
560 24     24 1 52 my $self = shift;
561 24         58 my $dbd = $self->gain_dbd;
562 24 50       101 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 1496 my $self = shift;
577 47 100       149 $self->header(TILT => shift) if @_;
578 47         100 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 1118 my $self = shift;
593 18 100       50 $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 18259 my $self = shift;
614 28         50 my $degrees = undef;
615 28         65 my $tilt = $self->tilt;
616 28 100       237 if (defined $tilt) {
617 27         90 $tilt =~ s/\A\s+//; #ltrim
618 27         71 $tilt =~ s/\s+\Z//; #rtrim
619 27 100       255 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         10 $degrees = abs($tilt + 0);
623             } elsif ($tilt =~ m/\A-?([0-9]{1,2})[-\s]deg.*ELECTRICAL/i) { #8-Deg Electrical
624 1         5 $degrees = $1 + 0;
625             } elsif ($tilt =~ m/\A-?([0-9]{1,2})[-\s]deg.*E-TILT/i ) { #8-Deg E-Tilt
626 1         4 $degrees = $1 + 0;
627             } elsif ($tilt =~ m/\A([0-9]{1,2})T\Z/i ) { #11T
628 2         9 $degrees = $1 + 0;
629             } elsif ($tilt =~ m/\AT([0-9]{1,2})\Z/i ) { #T11
630 2         7 $degrees = $1 + 0;
631             } elsif ($tilt =~ m/\AELECTRICAL -?([0-9]{1,2})\b/ ) { #ELECTRICAL 11...
632 2         9 $degrees = $1 + 0;
633             } elsif ($tilt =~ m/\AELECTRICAL\Z/i ) { #Spec: ELECTRICAL
634 11   100     33 my $comment = $self->comment // '';
635 11   100     104 my $electrical_tilt = $self->electrical_tilt // '';
636 11         94 $electrical_tilt =~ s/\A\s+//; #ltrim
637 11         29 $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         13 $degrees = $1 + 0;
646             }
647             }
648             }
649 28         148 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 2337 my $self = shift;
663 20 100       67 $self->header(COMMENT => shift) if @_;
664 20         44 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;