File Coverage

lib/Geo/Format/Envisat.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             # Copyrights 2008-2011 by Mark Overmeer.
2             # For other contributors see ChangeLog.
3             # See the manual pages for details on the licensing terms.
4             # Pod stripped from pm file by OODoc 2.00.
5 2     2   120937 use warnings;
  2         4  
  2         54  
6 2     2   9 use strict;
  2         4  
  2         74  
7              
8             package Geo::Format::Envisat;
9 2     2   21 use vars '$VERSION';
  2         3  
  2         109  
10             $VERSION = '0.03';
11              
12 2     2   8 use base 'Exporter';
  2         4  
  2         204  
13              
14             our @EXPORT = qw/envisat_mph_from_name envisat_meta_from_file/;
15              
16 2     2   9 use File::Basename qw/basename/;
  2         3  
  2         191  
17 2     2   842 use POSIX qw/tzset mktime strftime/;
  2         6408  
  2         13  
18 2     2   2069 use IO::Uncompress::AnyUncompress qw/$AnyUncompressError/;
  2         96655  
  2         271  
19 2     2   2210 use Geo::Point ();
  0            
  0            
20              
21             my @month = qw/XXX JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC/;
22             my %month;
23             $month{$_} = keys %month for @month;
24             delete $month{XXX};
25              
26             $ENV{TZ} = 'UTC';
27             tzset; # needed for mktime()
28              
29             use constant MPH_LENGTH => 1247;
30              
31             sub _decode_name($);
32             sub _cleanup_mph($);
33             sub _cleanup_sph($);
34             sub _cleanup_dsd($);
35             sub _read_block($$);
36             sub _decompose($);
37             sub _timestamp_to_time($);
38             sub _timestamp_iso($);
39             sub _add_missing_stripped($);
40             sub _strip_unit($);
41              
42              
43             sub envisat_mph_from_name($)
44             { my $filename = shift;
45             my $mph = _decode_name $filename;
46              
47             if(my $size = -s $filename)
48             { $mph->{TOT_SIZE} = "$size";
49             }
50              
51             my ($year, $month, $day) = $mph->{start_day} =~ m/(\d{4})(\d\d)(\d\d)/;
52             my ($hour, $min, $sec ) = $mph->{start_time} =~ m/(\d\d)(\d\d)(\d\d)/;
53             $mph->{SENSING_START} = sprintf "%02d-%s-%04d %02d:%02d:%02d.%06d"
54             , $day, $month[$month], $year, $hour, $min, $sec, 0;
55              
56             _cleanup_mph($mph);
57             $mph;
58             }
59              
60              
61             sub envisat_meta_from_file($@)
62             { my ($fn, %args) = @_;
63             my $mph_from_filename = _decode_name $fn;
64              
65             my $meta;
66             my $file = IO::Uncompress::AnyUncompress->new($fn)
67             or die "ERROR: cannot read from $fn: $AnyUncompressError\n";
68              
69             my $mph = _decompose(_read_block $file, MPH_LENGTH);
70              
71             foreach my $k (keys %$mph_from_filename)
72             { if(! exists $mph->{$k} )
73             { $mph->{$k} = $mph_from_filename->{$k};
74             }
75             elsif($mph->{$k} ne $mph_from_filename->{$k})
76             { warn "field $k differs: \n in file : `$mph->{$k}'\n"
77             , " in filename: `$mph_from_filename->{$k}'\n";
78             }
79             }
80              
81             _cleanup_mph $mph;
82              
83             my ($sph, %dsd);
84             if(my $sphsize = $mph->{sph_size})
85             { my $dsd_size = $mph->{dsd_size};
86             my $dsd_num = $mph->{num_dsd};
87             my $sph_length = $sphsize - $dsd_size * $dsd_num;
88             $sph = _decompose(_read_block $file, $sph_length);
89             _cleanup_sph $sph;
90              
91             for(1..$dsd_num)
92             { my $dsd = _decompose(_read_block $file, $dsd_size);
93             _cleanup_dsd $dsd;
94             $dsd->{num_dsr} > 0
95             or next;
96             (my $name = $dsd->{ds_name}) =~ s/\s+/_/g;
97             $dsd{$name} = $dsd;
98             }
99              
100             # only forward seeks permitted.
101             my $take = exists $args{take_dsd_content} ? $args{take_dsd_content} : 0;
102             if($take)
103             { my @sorted = sort {$a->{ds_offset} <=> $b->{ds_offset}} values %dsd;
104             foreach my $dsd (@sorted)
105             { next if $dsd->{DS_TYPE} eq 'M';
106             $file->seek($dsd->{ds_offset}, 0);
107             $dsd->{content} = _read_block $file, $dsd->{ds_size};
108             }
109             }
110             }
111              
112             $file->close;
113              
114             +{ mph => $mph, sph => $sph, dsd => \%dsd };
115             }
116              
117             ##### some helpers
118              
119             sub _decode_name($)
120             { my $name = uc basename shift;
121             $name =~
122             m/^ (\w{10}) # product ID
123             (\w) # processing stage flag
124             (\w{3}) # originator ID
125             (\d{8}) _ # start_day
126             (\d{6}) _ # start_time
127             (\d{8}) # duration
128             (\w) # phase id
129             (\d{3}) _ # cycle number within phase
130             (\d{5}) _ # relative orbit number within cycle
131             (\d{5}) _ # absolute orbit number
132             (\d{4}) # product file counter
133             \. (\w\w) # satellite ID
134             /x
135             or return ();
136              
137             { PRODUCT_ID => $1
138             , PROC_STAGE => $2
139             , originator_id => $3 # proc center abbreviated into 3 chars
140             , start_day => $4
141             , start_time => $5
142             , duration => $6+0
143             , PHASE => $7
144             , CYCLE => "+$8"
145             , REL_ORBIT => "+$9"
146             , ABS_ORBIT => "+$10"
147             , product_file_counter => $11
148             , satellite_id => $12
149             };
150             }
151              
152             sub _cleanup_mph($)
153             { my $mph = shift;
154              
155             if(my $s = $mph->{PROC_STAGE})
156             { $mph->{proc_stage}
157             = $s eq 'N' ? 'Near Real Time'
158             : $s eq 'T' ? 'test product'
159             : $s eq 'V' ? 'fully validated'
160             : $s eq 'S' ? 'special product'
161             : $s eq 'X' ? 'not used'
162             : $s gt 'N' && $s lt 'V' ? "consolidation level $s"
163             : 'ERROR';
164             }
165              
166             if(my $s = $mph->{satellite_id})
167             { $mph->{satellite}
168             = $s eq 'N1' ? 'Envisat'
169             : $s eq 'E1' ? 'ERS1'
170             : $s eq 'E2' ? 'ERS2'
171             : 'ERROR';
172             }
173              
174             if(my $s = $mph->{VECTOR_SOURCE})
175             { $mph->{vector_source}
176             = $s eq 'FP' ? 'FOS predicted orbit state vectors'
177             : $s eq 'DN' ? 'DORIS Level 0 navigator product acquired at PDHS'
178             : $s eq 'FR' ? 'FOS restituted orbit state vectors'
179             : $s eq 'DI' ? 'DORIS initial (prelimary) orbit'
180             : $s eq 'DP' ? 'DORIS precise orbit'
181             : $s eq '' ? 'not used'
182             : 'ERROR';
183             }
184              
185             # add lower-cased field versions without unit specification
186             foreach my $field (qw/TOT_SIZE SPH_SIZE DSD_SIZE DELTA_UT1 CLOCK_STEP
187             X_POSITION Y_POSITION Z_POSITION X_VELOCITY Y_VELOCITY Z_VELOCITY/)
188             { $mph->{lc $field} = _strip_unit $mph->{$field}
189             if $mph->{$field};
190             }
191              
192             # make numeric
193             foreach my $field (qw/CYCLE REL_ORBIT ABS_ORBIT LEAP_SIGN
194             NUM_DATA_SETS NUM_DSD SAT_BINARY_TIME PRODUCT_ERR/)
195             { defined $mph->{$field} or next;
196             $mph->{lc $field} = $mph->{$field} + 0;
197             }
198              
199             # convert to OS time
200             foreach my $field (qw/SENSING_START SENSING_STOP PROC_TIME
201             STATE_VECTOR_TIME UTC_SBT_TIME LEAP_UTC/)
202             { defined $mph->{$field} or next;
203             $mph->{lc $field} = _timestamp_to_time $mph->{$field};
204             $mph->{lc($field).'_iso'} = _timestamp_iso $mph->{lc $field};
205             }
206              
207             if(my $station = $mph->{ACQUISITION_STATION})
208             { $station =~ s/\s//g;
209             $mph->{acquisition_station} = [ split /\,/, $station ];
210             }
211              
212             _add_missing_stripped $mph;
213             }
214              
215             sub _cleanup_sph($)
216             { my $sph = shift;
217            
218             # add field versions without unit specification
219             $sph->{lc $_} = _strip_unit $sph->{$_}
220             for qw/AZIMUTH_SPACING LINE_LENGTH LINE_TIME_INTERVAL RANGE_SPACING/;
221              
222             # convert to OS time
223             foreach my $field (qw/FIRST_LINE_TIME LAST_LINE_TIME/)
224             { $sph->{lc $field} = _timestamp_to_time $sph->{$field};
225             $sph->{lc($field).'_iso'} = _timestamp_iso $sph->{lc $field};
226             }
227              
228             # make numeric
229             foreach my $field (qw/AZIMUTH_LOOKS NUM_SLICES RANGE_LOOKS
230             SLICE_POSITION STRIPLINE_CONTINUITY_INDICATOR/)
231             { $sph->{lc $field} = $sph->{$field} + 0;
232             }
233              
234             if(my $t = $sph->{DATA_TYPE})
235             { $sph->{pixel_octets}
236             = $t eq 'UWORD' ? 2 # unsigned
237             : $t eq 'SWORD' ? 2 # signed
238             : $t eq 'UBYTE' ? 1 # unsigned
239             : 'ERROR';
240             }
241              
242             # Simplify coordinates
243             my %bounds;
244             foreach my $azimuth ('FIRST', 'LAST')
245             { foreach my $range ('NEAR', 'MID', 'FAR')
246             { (my $lat = $sph->{"${azimuth}_${range}_LAT"}) =~ s/\<.*//;
247             $lat *= 1e-6;
248             $sph->{lc "${azimuth}_${range}_lat"} = $lat;
249             (my $long = $sph->{"${azimuth}_${range}_LONG"}) =~ s/\<.*//;
250             $long *= 1e-6;
251             $sph->{lc "${azimuth}_${range}_long"} = $long;
252              
253             my $point = Geo::Point->latlong($lat, $long, 'wgs84');
254             push @{$bounds{$azimuth}}, $point;
255             $sph->{lc "${azimuth}_${range}_point"} = $point;
256             }
257             }
258              
259             # I don't know how multiple polys should be calculated (when there
260             # is strip spacing. Polygon clockwise starting left-top
261             my @poly
262             = $sph->{PASS} eq 'DESCENDING'
263             ? (@{$bounds{FIRST}}, reverse @{$bounds{LAST}} )
264             : (@{$bounds{LAST}}, reverse @{$bounds{FIRST}});
265              
266             my $footprint = Geo::Line->filled(points => \@poly, clockwise => 1);
267             $sph->{target_polys} = Geo::Surface->new($footprint);
268              
269             _add_missing_stripped $sph;
270             }
271              
272             sub _cleanup_dsd($)
273             { my $dsd = shift;
274              
275             $dsd->{lc $_} = _strip_unit $dsd->{$_}
276             for qw/DS_OFFSET DS_SIZE DSR_SIZE/;
277              
278             $dsd->{num_dsr} = $dsd->{NUM_DSR} + 0;
279              
280             my $t = $dsd->{DS_TYPE};
281             $dsd->{ds_type}
282             = $t eq 'M' ? 'Measurement DS'
283             : $t eq 'A' ? 'Annotation DS'
284             : $t eq 'G' ? 'Global ADS'
285             : $t eq 'R' ? 'Reference only'
286             : 'ERROR';
287              
288             # field can also contain 'MISSING' and 'NOT USED' :-(
289             $dsd->{filename} = $t eq 'R' ? $dsd->{FILENAME} : undef;
290              
291             _add_missing_stripped $dsd;
292             }
293              
294             sub _read_block($$)
295             { my ($fh, $size) = @_;
296             my $buffer = '';
297             $fh->read($buffer, $size - length $buffer, length $buffer)
298             while length $buffer < $size;
299             $buffer;
300             }
301              
302             sub _decompose($)
303             { my @lines = split /\n/, shift;
304             my %h;
305              
306             foreach my $line (@lines)
307             { if ($line =~ m/^(\w+)\=\"(.*?)"/) { $h{$1} = $2 }
308             elsif($line =~ m/^(\w+)\=(.*)/) { $h{$1} = $2 }
309             }
310             \%h;
311             }
312              
313             sub _timestamp_to_time($)
314             { my $stamp = shift;
315             $stamp =~ m/^(\d\d)\-([A-Z]{3})\-(\d{4}) (\d\d):(\d\d):(\d\d)\.(\d+)/
316             or return 'ERROR';
317             my $secs = mktime($6, $5, $4, $1, $month{$2}-1, $3-1900);
318             0.0 + "$secs.$7";
319             }
320              
321             sub _timestamp_iso($)
322             { my $time = shift;
323             my $frag = $time =~ s/\.(\d+)$// ? $1 : 0;
324             (strftime "%FT%T", gmtime $time). ($frag ? ".$frag" : '') . 'Z';
325             }
326              
327             sub _add_missing_stripped($)
328             { my $h = shift;
329              
330             # for all fields without a lower-cased value, we generate one
331             # where the blanks are stripped off.
332             foreach my $f (keys %$h)
333             { next if exists $h->{lc $f}; # skips lc keys as well
334             ($h->{lc $f} = $h->{$f}) =~ s/\s+$//;
335             }
336             }
337              
338             sub _strip_unit($)
339             { (my $value = $_[0]) =~ s/\<[^<>]*\>\s*$//;
340             $value + 0;
341             }
342              
343             1;