File Coverage

lib/Geo/Format/Landsat/MTL.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1             # Copyrights 2009 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 1.06.
5 1     1   2261 use warnings;
  1         2  
  1         42  
6 1     1   7 use strict;
  1         3  
  1         52  
7              
8             package Geo::Format::Landsat::MTL;
9 1     1   19 use vars '$VERSION';
  1         2  
  1         80  
10             $VERSION = '0.03';
11              
12 1     1   6 use base 'Exporter';
  1         2  
  1         132  
13              
14             our @EXPORT = qw/
15             landsat_mtl_from_file
16             landsat_meta_from_filename
17             /;
18              
19 1     1   6 use constant METADATA_RECORD => 65536;
  1         2  
  1         80  
20 1     1   6 use constant METERS2FEET => 3.2808399;
  1         2  
  1         43  
21              
22 1     1   426 use Geo::Point ();
  0            
  0            
23             use File::Basename qw/basename/;
24              
25             use POSIX qw/mktime strftime tzset/;
26             $ENV{TZ} = 'UTC'; tzset;
27              
28             sub _process_group($);
29              
30             sub _cleanup_mtl($);
31             sub _cleanup_product_parameters($);
32             sub _cleanup_product_metadata($$);
33             sub _cleanup_metadata_file_info($);
34             sub _cleanup_min_max_radiance($);
35             sub _cleanup_min_max_pixel_value($);
36             sub _cleanup_projection_parameters($);
37             sub _cleanup_corrections_applied($);
38             sub _get_map_proj($);
39              
40              
41             sub landsat_meta_from_filename($)
42             { my $filename = basename shift;
43             $filename =~ m/^L([57])(\d\d\d)(\d\d\d)_(\d\d\d)(\d\d\d\d)(\d\d)(\d\d)/
44             or return;
45              
46             +{ SPACECRAFT_ID => "Landsat$1"
47             , WRS_PATH => $2
48             , STARTING_ROW => $3
49             , ENDING_ROW => 4
50             , ACQUISITION_DATE => "$5-$6-$7"
51             };
52             }
53              
54              
55             sub landsat_mtl_from_file($)
56             { my $f = shift;
57              
58             my $text;
59             if(UNIVERSAL::isa($f, 'GLOB') || UNIVERSAL::isa($f, 'IO::Handle'))
60             { sysread $f, $text, METADATA_RECORD;
61             }
62             else
63             { open F, '<', $f
64             or die "ERROR: cannot read from $f: $!\n";
65             sysread F, $text, METADATA_RECORD;
66             close F;
67             }
68              
69             $text =~ s/\0+\z//; # record padded with \0 bytes
70              
71             $text =~ s/^\s*END\s*\z//m
72             or die "ERROR: did not get 'END' tag\n";
73              
74             my $wrapper = _process_group $text;
75             my ($type, $data) = %$wrapper; # only one key
76              
77             ($type, _cleanup_mtl $data);
78             }
79              
80             sub _process_group($)
81             { my $text = shift;
82             my $data;
83              
84             while($text =~ s/\A\s*GROUP\s*\=\s*(\w+)\s*
85             (.*?)
86             \s*END_GROUP\s*\=\s*\1\s*//xsm)
87             { my $name = $1;
88             $data->{$name} = _process_group($2);
89             }
90              
91             foreach my $line (split /\n/, $text)
92             { if($line =~ m/^\s*(\w+)\s*\=\s+\"(.*?)\"\s*$/ )
93             { $data->{$1} = $2;
94             }
95             elsif($line =~ m/^\s*(\w+)\s*\=\s+(.*?)\s*$/ )
96             { $data->{$1} = $2;
97             }
98             else
99             { warn "Do not understand line:\n $line\n";
100             }
101             }
102              
103             $data;
104             }
105              
106             sub _cleanup_mtl($)
107             { my $data = shift;
108              
109             _cleanup_metadata_file_info $data->{METADATA_FILE_INFO};
110             _cleanup_min_max_radiance $data->{MIN_MAX_RADIANCE};
111             _cleanup_min_max_pixel_value $data->{MIN_MAX_PIXEL_VALUE};
112             _cleanup_product_parameters $data->{PRODUCT_PARAMETERS};
113             _cleanup_corrections_applied $data->{CORRECTIONS_APPLIED};
114             _cleanup_projection_parameters $data->{PROJECTION_PARAMETERS};
115              
116             my $mapproj = $data->{map_projection} = _get_map_proj $data;
117              
118             _cleanup_product_metadata $data->{PRODUCT_METADATA}, $mapproj;
119             $data;
120             }
121              
122             sub _cleanup_metadata_file_info($)
123             { my $d = shift or return;
124              
125             if($d->{REQUEST_ID} =~ m/(...)(\d\d)(\d\d)(\d\d)(\d{4})_(\d{5})/)
126             { my $date = sprintf "%04d-%02d-%02dZ"
127             , ($2 < 70 ? 2000+$2 : 1900+$2), $3, $4;
128             $d->{request_id} = { node => $1+0, date_iso => $date, seqnr => $5+0
129             , dorran_unit => $6+0 };
130             }
131              
132             # 0 = unknown, becomes undef.
133             $d->{landsat_xband} = $d->{LANDSAT5_XBAND} || $d->{LANDSAT7_XBAND} || undef;
134              
135             # no simple way to translate DOY -> month/day
136             if($d->{DATEHOUR_CONTACT_PERIOD} =~ m/^(\d\d)(\d\d\d)(\d\d)$/ )
137             { my ($year, $yday, $hour) = ($1, $2, $3);
138             $year += $year < 70 ? 2000 : 1900;
139             my @monthdays = (undef, 31,28,31,30,31,30,31,31,30,31,30,31);
140             $monthdays[2] = 29 if $year%400==0 || ($year%4==0 && $year%100!=0);
141             my ($month, $day) = (1, $yday);
142             while($day > $monthdays[$month])
143             { $day -= $monthdays[$month];
144             $month++;
145             }
146             $d->{received} = sprintf "%04d-%02d-%02dT%02d:00:00Z"
147             , $year, $month, $day, $hour;
148             }
149             }
150              
151             sub _cleanup_product_metadata($$)
152             { my ($d, $proj) = @_;
153             $d or return;
154             my $mapproj = $proj->nick;
155              
156             if($d->{PROCESSING_SOFTWARE} =~ m/([A-Z]+)_(.*)/)
157             { $d->{software_system} = $1;
158             $d->{software_version} = $2;
159             }
160             $d->{EPHEMERIS_TYPE} ||= 'PREDICTIVE';
161              
162             foreach my $band ( '', '_PAN', '_THM')
163             { $d->{"PRODUCT_UL_CORNER_LAT$band"} or next;
164             my (@bbox, @bbox_map);
165             foreach my $c (qw/UL UR LR LL/)
166             { my $lat = $d->{"PRODUCT_${c}_CORNER_LAT${band}"};
167             my $lon = $d->{"PRODUCT_${c}_CORNER_LON${band}"};
168             my $corner = Geo::Point->latlong($lat, $lon, 'wgs84');
169             $d->{"product_".(lc $c)."_wgs84".lc $band} = $corner;
170             push @bbox, $corner;
171              
172             my $x = $d->{"PRODUCT_${c}_CORNER_MAPX${band}"};
173             my $y = $d->{"PRODUCT_${c}_CORNER_MAPY${band}"};
174             my $map = Geo::Point->xy($x, $y, $mapproj);
175             $d->{"product_".(lc $c)."_map".lc $band} = $map;
176             push @bbox_map, $map;
177             }
178             my $bbox = Geo::Line->filled(points => \@bbox, clockwise => 1);
179             $d->{"footprint".lc $band} = Geo::Surface->new($bbox);
180              
181             my $bbox_map = Geo::Line->filled(points => \@bbox_map, clockwise => 1);
182             $d->{"footprint_map".lc $band} = Geo::Surface->new($bbox_map);
183             }
184             }
185              
186             sub _cleanup_min_max_radiance($)
187             { my $d = shift or return;
188             # strings can be used as numbers... nothing to do
189             }
190              
191             sub _cleanup_min_max_pixel_value($)
192             { my $d = shift or return;
193             # strings can be used as numbers... nothing to do
194             }
195              
196             sub _cleanup_product_parameters($)
197             { my $d = shift or return;
198             # too specific
199             }
200              
201             sub _cleanup_corrections_applied($)
202             { my $d = shift or return;
203              
204             foreach my $key (qw/BANDING COHERENT_NOICE MEMORY_EFFECT
205             SCAN_CORRELATED_SHIFT INOPERABLE_DETECTORS DROPPED_LINES/)
206             { defined $d->{$key} or next;
207             $d->{lc $key} = $d->{$key} eq 'Y' ? 1 : 0;
208             }
209             }
210              
211             sub _cleanup_projection_parameters($)
212             { my $d = shift or return;
213              
214             $d->{REFERENCE_DATUM} eq 'WGS84' # hard-coded in spec
215             && $d->{REFERENCE_ELLIPSOID} eq 'WGS84'
216             or die "ERROR: WGS84 expected\n";
217              
218             if(my $o = $d->{ORIENTATION})
219             { $d->{orientation}
220             = $o eq 'NOM' ? 'Nominal Path'
221             : $o eq 'NUP' ? 'North Up'
222             : $o eq 'TN' ? 'True North'
223             : $o eq 'USR' ? 'User'
224             : 'UNKNOWN';
225             }
226              
227             if(my $r = $d->{RESAMPLING_OPTION})
228             { $d->{resampling_option}
229             = $r eq 'NN' ? 'Nearest Neighbor'
230             : $r eq 'CC' ? 'Cubic Convolution'
231             : $r eq 'MTF' ? 'Modulation Transfer Function'
232             : $r eq 'BI' ? 'Bilinear'
233             : $r eq 'KD' ? 'Kaiser Damped'
234             : $r eq '16' ? '16 Point Sinc'
235             : $r eq '8' ? '8 Point Sinc'
236             : $r eq 'DW' ? 'Damped Window'
237             : 'UNKNOWN';
238             }
239             }
240              
241              
242             # See http://www.remotesensing.org/geotiff/proj_list
243             sub _get_map_proj($)
244             { my $data = shift;
245             my $code = $data->{PROJECTION_PARAMETERS}{MAP_PROJECTION};
246             my $details = $data->{"${code}_PARAMETERS"};
247            
248             my $nick = lc $code;
249             my ($proj, $name, @params);
250              
251             my $units = $details->{FALSE_EASTING_NORTHING_UNITS};
252             push @params, '-M'
253             if defined $units && $units eq 'meters';
254              
255             my @common = qw/
256             FALSE_EASTING x_0
257             FALSE_NORTHING y_0
258             LATITUDE_OF_PROJECTION_ORIGIN lat_0
259             LATITUDE_OF_CENTER lat_0
260             LONGITUDE_OF_CENTRAL_MERIDIAN lon_0
261             LONGITUDE_OF_CENTER lon_0
262             VERTICAL_LONGITUDE_FROM_POLE lon_0
263             LATITUDE_OF_FIRST_STANDARD_PARALLEL lat_1
264             LATITUDE_FIRST_POINT_GEODETIC lat_1
265             LONGITUDE_OF_FIRST_STANDARD_PARALLEL lon_1
266             LONGITUDE_FIRST_POINT_GEODETIC lon_1
267             LATITUDE_OF_SECOND_STANDARD_PARALLEL lat_2
268             LATITUDE_SECOND_POINT_GEODETIC lat_2
269             LONGITUDE_SECOND_POINT_GEODETIC lon_2
270             LATITUDE_OF_TRUE_SCALE lat_ts
271             ANGLE_OF_AZIMUTH alpha
272             LONGITUDE_ALONG_PROJECTION lonc
273             SCALE_FACTOR_AT_CENTRAL_MERIDIAN k
274             /;
275              
276             while(@common)
277             { my ($key, $label) = (shift @common, shift @common);
278             push @params, $label => $details->{$key}
279             if $details->{$key};
280             }
281              
282             my $h = $details->{HEIGHT}; # always in meters, according to doc
283             if(defined $h)
284             { push @params, h => ($units eq 'meters' ? $h : $h * METERS2FEET);
285             }
286              
287             # convert to NLAPS type
288             $code .= $details->{EQC_TYPE} if $code eq 'EQC';
289             $code .= $details->{OM_TYPE} if $code eq 'OM';
290             $code .= $details->{SOM_TYPE} if $code eq 'SOM';
291              
292             if($code eq 'AKC')
293             { $name = 'Alaska Conformal';
294             $proj = 'UNKNOWN'; #???
295             }
296             elsif($code eq 'AEA') # epsg:9822
297             { $name = 'Albers Equal-Area Conic';
298             $proj = 'eae';
299             }
300             elsif($code eq 'AZIM')
301             { $name = 'Azimuthal Equidistant';
302             $proj = 'aeqd';
303             }
304             elsif($code =~ m/^EQC([AB])$/)
305             { $name = "Equidistant Conic type $1";
306             $proj = 'eqdc';
307             }
308             elsif($code eq 'EQUI') # epsg:9823 (spherical), 9842 (elliptical)
309             { $name = 'Equirectangular';
310             $proj = 'UNKNOWN'; #???
311             }
312             elsif($code eq 'GNOM')
313             { $name = 'Gnomonic';
314             $proj = 'gnom';
315             }
316             elsif($code eq 'GVNP')
317             { $name = 'General Vertical Near Side Perspective';
318             $proj = 'nsper';
319             }
320             elsif($code eq 'HAMM')
321             { $name = 'Hammer';
322             $proj = 'hammer';
323             }
324             elsif($code eq 'LAEA') # epsg:9820
325             { $name = 'Lambert Azimuthal Equal Area';
326             $proj = 'laea';
327             }
328             elsif($code eq 'LCC') # epsg:9802 (?)
329             { $name = 'Lambert Conformal Conic (2SP)';
330             $proj = 'lcc';
331             }
332             elsif($code eq 'MERC')
333             { $name = 'Mercator (2SP)';
334             $proj = 'merc';
335             }
336             elsif($code eq 'MCYL')
337             { $name = 'Miller Cylindrical';
338             $proj = 'mill';
339             }
340             elsif($code eq 'MOLL')
341             { $name = 'Mollweide';
342             $proj = 'moll';
343             }
344             elsif($code eq 'OEA')
345             { $name = 'Oblated Equal Area';
346             $proj = 'oea';
347             push @params, theta => $details->{ANGLE};
348             }
349             elsif($code =~ m/^OM([AB])$/) # epsg:9815
350             { $name = "Oblique Mercator type $1";
351             $proj = 'omerc';
352             #??? SCALE_FACTOR_AT_CENTER_OF_PROJECTION
353             }
354             elsif($code eq 'ORTH')
355             { $name = 'Orthographic';
356             $proj = 'ortho';
357             }
358             elsif($code eq 'PC')
359             { $name = 'Polyconic'; # (American?)
360             $proj = 'poly'; # ???
361             }
362             elsif($code eq 'PS') # epsg:9810
363             { $name = 'Polar Stereographic';
364             $proj = 'stere';
365             push @params, lat_0 => '90'; #???
366             }
367             elsif($code eq 'ROBN')
368             { $name = 'Robinson';
369             $proj = 'robin';
370             }
371             elsif($code eq 'SINU')
372             { $name = 'Sinusoidal (Sanson-Flamsteed)';
373             $proj = 'sinu';
374             }
375             elsif($code eq 'SOMA')
376             { $name = 'Space Oblique Mercator type A';
377             $proj = "UNKNOWN"; # too complex
378             }
379             elsif($code eq 'SOMB')
380             { $name = 'Space Oblique for Landsat';
381             $proj = 'lsat';
382             push @params, lsat => $details->{LANDSAT_NUMBER}
383             , path => $details->{PATH};
384             }
385             elsif($code eq 'STRG')
386             { $name = 'Stereographic';
387             $proj = 'stere';
388             }
389             elsif($code eq 'TM')
390             { $name = 'Traverse Mercator (Gauss-Krueger)';
391             $proj = 'tmerc';
392             }
393             elsif($code eq 'UTM')
394             { $name = 'Universal Transverse Mercator';
395             my $zone = $details->{ZONE_NUMBER};
396             $nick = "utm$zone-wgs84";
397             $proj = 'utm';
398             push @params, datum => 'WGS84', zone => $zone;
399             }
400             elsif($code eq 'VDGR')
401             { $name = 'van der Grinten';
402             $proj = 'vandg';
403             }
404             elsif($code eq 'WIV')
405             { $name = 'Wagner IV';
406             $proj = 'wag4';
407             }
408             elsif($code eq 'WVII')
409             { $name = 'Wagner VII';
410             $proj = 'wag7';
411             }
412             else
413             { die "ERROR: unknown projection code $code\n";
414             }
415              
416             unshift @params, proj => $proj;
417              
418             Geo::Proj->new
419             ( nick => $nick
420             , name => $name
421             , proj4 => \@params
422             );
423             }
424              
425             1;