File Coverage

blib/lib/Image/ExifTool/GPS.pm
Criterion Covered Total %
statement 74 81 91.3
branch 40 58 68.9
condition 15 28 53.5
subroutine 7 7 100.0
pod 0 4 0.0
total 136 178 76.4


line stmt bran cond sub pod time code
1             #------------------------------------------------------------------------------
2             # File: GPS.pm
3             #
4             # Description: EXIF GPS meta information tags
5             #
6             # Revisions: 12/09/2003 - P. Harvey Created
7             #------------------------------------------------------------------------------
8              
9             package Image::ExifTool::GPS;
10              
11 68     68   4880 use strict;
  68         281  
  68         2294  
12 68     68   385 use vars qw($VERSION);
  68         147  
  68         2666  
13 68     68   5556 use Image::ExifTool::Exif;
  68         427  
  68         168289  
14              
15             $VERSION = '1.54';
16              
17             my %coordConv = (
18             ValueConv => 'Image::ExifTool::GPS::ToDegrees($val)',
19             ValueConvInv => 'Image::ExifTool::GPS::ToDMS($self, $val)',
20             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1)',
21             );
22              
23             %Image::ExifTool::GPS::Main = (
24             GROUPS => { 0 => 'EXIF', 1 => 'GPS', 2 => 'Location' },
25             WRITE_PROC => \&Image::ExifTool::Exif::WriteExif,
26             CHECK_PROC => \&Image::ExifTool::Exif::CheckExif,
27             WRITABLE => 1,
28             WRITE_GROUP => 'GPS',
29             0x0000 => {
30             Name => 'GPSVersionID',
31             Writable => 'int8u',
32             Mandatory => 1,
33             Count => 4,
34             PrintConv => '$val =~ tr/ /./; $val',
35             PrintConvInv => '$val =~ tr/./ /; $val',
36             },
37             0x0001 => {
38             Name => 'GPSLatitudeRef',
39             Writable => 'string',
40             Notes => q{
41             tags 0x0001-0x0006 used for camera location according to MWG 2.0. ExifTool
42             will also accept a number when writing GPSLatitudeRef, positive for north
43             latitudes or negative for south, or a string containing N, North, S or South
44             },
45             Count => 2,
46             PrintConv => {
47             # extract N/S if written from Composite:GPSLatitude
48             # (also allow writing from a signed number)
49             OTHER => sub {
50             my ($val, $inv) = @_;
51             return undef unless $inv;
52             return uc $2 if $val =~ /(^|[^A-Z])([NS])(orth|outh)?\b/i;
53             return $1 eq '-' ? 'S' : 'N' if $val =~ /([-+]?)\d+/;
54             return undef;
55             },
56             N => 'North',
57             S => 'South',
58             },
59             },
60             0x0002 => {
61             Name => 'GPSLatitude',
62             Writable => 'rational64u',
63             Count => 3,
64             %coordConv,
65             PrintConvInv => 'Image::ExifTool::GPS::ToDegrees($val,undef,"lat")',
66             },
67             0x0003 => {
68             Name => 'GPSLongitudeRef',
69             Writable => 'string',
70             Count => 2,
71             Notes => q{
72             ExifTool will also accept a number when writing this tag, positive for east
73             longitudes or negative for west, or a string containing E, East, W or West
74             },
75             PrintConv => {
76             # extract E/W if written from Composite:GPSLongitude
77             # (also allow writing from a signed number)
78             OTHER => sub {
79             my ($val, $inv) = @_;
80             return undef unless $inv;
81             return uc $2 if $val =~ /(^|[^A-Z])([EW])(ast|est)?\b/i;
82             return $1 eq '-' ? 'W' : 'E' if $val =~ /([-+]?)\d+/;
83             return undef;
84             },
85             E => 'East',
86             W => 'West',
87             },
88             },
89             0x0004 => {
90             Name => 'GPSLongitude',
91             Writable => 'rational64u',
92             Count => 3,
93             %coordConv,
94             PrintConvInv => 'Image::ExifTool::GPS::ToDegrees($val,undef,"lon")',
95             },
96             0x0005 => {
97             Name => 'GPSAltitudeRef',
98             Writable => 'int8u',
99             Notes => q{
100             ExifTool will also accept number when writing this tag, with negative
101             numbers indicating below sea level
102             },
103             PrintConv => {
104             OTHER => sub {
105             my ($val, $inv) = @_;
106             return undef unless $inv and $val =~ /^([-+0-9])/;
107             return($1 eq '-' ? 1 : 0);
108             },
109             0 => 'Above Sea Level',
110             1 => 'Below Sea Level',
111             },
112             },
113             0x0006 => {
114             Name => 'GPSAltitude',
115             Writable => 'rational64u',
116             # extricate unsigned decimal number from string
117             ValueConvInv => '$val=~/((?=\d|\.\d)\d*(?:\.\d*)?)/ ? $1 : undef',
118             PrintConv => '$val =~ /^(inf|undef)$/ ? $val : "$val m"',
119             PrintConvInv => '$val=~s/\s*m$//;$val',
120             },
121             0x0007 => {
122             Name => 'GPSTimeStamp',
123             Groups => { 2 => 'Time' },
124             Writable => 'rational64u',
125             Count => 3,
126             Shift => 'Time',
127             Notes => q{
128             UTC time of GPS fix. When writing, date is stripped off if present, and
129             time is adjusted to UTC if it includes a timezone
130             },
131             ValueConv => 'Image::ExifTool::GPS::ConvertTimeStamp($val)',
132             ValueConvInv => '$val=~tr/:/ /;$val',
133             PrintConv => 'Image::ExifTool::GPS::PrintTimeStamp($val)',
134             # pull time out of any format date/time string
135             # (converting to UTC if a timezone is given)
136             PrintConvInv => sub {
137             my ($v, $et) = @_;
138             $v = $et->TimeNow() if lc($v) eq 'now';
139             my @tz;
140             if ($v =~ s/([-+])(\d{1,2}):?(\d{2})\s*(DST)?$//i) { # remove timezone
141             my $s = $1 eq '-' ? 1 : -1; # opposite sign to convert back to UTC
142             my $t = $2;
143             @tz = ($s*$2, $s*$3);
144             }
145             # (note: we must allow '.' as a time separator, eg. '10.30.00', with is tricky due to decimal seconds)
146             # YYYYmmddHHMMSS[.ss] format
147             my @a = ($v =~ /^[^\d]*\d{4}[^\d]*\d{1,2}[^\d]*\d{1,2}[^\d]*(\d{1,2})[^\d]*(\d{2})[^\d]*(\d{2}(?:\.\d+)?)[^\d]*$/);
148             # HHMMSS[.ss] format
149             @a or @a = ($v =~ /^[^\d]*(\d{1,2})[^\d]*(\d{2})[^\d]*(\d{2}(?:\.\d+)?)[^\d]*$/);
150             @a or warn('Invalid time (use HH:MM:SS[.ss][+/-HH:MM|Z])'), return undef;
151             if (@tz) {
152             # adjust to UTC
153             $a[1] += $tz[1];
154             $a[0] += $tz[0];
155             while ($a[1] >= 60) { $a[1] -= 60; ++$a[0] }
156             while ($a[1] < 0) { $a[1] += 60; --$a[0] }
157             $a[0] = ($a[0] + 24) % 24;
158             }
159             return join(':', @a);
160             },
161             },
162             0x0008 => {
163             Name => 'GPSSatellites',
164             Writable => 'string',
165             },
166             0x0009 => {
167             Name => 'GPSStatus',
168             Writable => 'string',
169             Count => 2,
170             PrintConv => {
171             A => 'Measurement Active', # Exif2.2 "Measurement in progress"
172             V => 'Measurement Void', # Exif2.2 "Measurement Interoperability" (WTF?)
173             # (meaning for 'V' taken from status code in NMEA GLL and RMC sentences)
174             },
175             },
176             0x000a => {
177             Name => 'GPSMeasureMode',
178             Writable => 'string',
179             Count => 2,
180             PrintConv => {
181             2 => '2-Dimensional Measurement',
182             3 => '3-Dimensional Measurement',
183             },
184             },
185             0x000b => {
186             Name => 'GPSDOP',
187             Description => 'GPS Dilution Of Precision',
188             Writable => 'rational64u',
189             },
190             0x000c => {
191             Name => 'GPSSpeedRef',
192             Writable => 'string',
193             Count => 2,
194             PrintConv => {
195             K => 'km/h',
196             M => 'mph',
197             N => 'knots',
198             },
199             },
200             0x000d => {
201             Name => 'GPSSpeed',
202             Writable => 'rational64u',
203             },
204             0x000e => {
205             Name => 'GPSTrackRef',
206             Writable => 'string',
207             Count => 2,
208             PrintConv => {
209             M => 'Magnetic North',
210             T => 'True North',
211             },
212             },
213             0x000f => {
214             Name => 'GPSTrack',
215             Writable => 'rational64u',
216             },
217             0x0010 => {
218             Name => 'GPSImgDirectionRef',
219             Writable => 'string',
220             Count => 2,
221             PrintConv => {
222             M => 'Magnetic North',
223             T => 'True North',
224             },
225             },
226             0x0011 => {
227             Name => 'GPSImgDirection',
228             Writable => 'rational64u',
229             },
230             0x0012 => {
231             Name => 'GPSMapDatum',
232             Writable => 'string',
233             },
234             0x0013 => {
235             Name => 'GPSDestLatitudeRef',
236             Writable => 'string',
237             Notes => 'tags 0x0013-0x001a used for subject location according to MWG 2.0',
238             Count => 2,
239             PrintConv => { N => 'North', S => 'South' },
240             },
241             0x0014 => {
242             Name => 'GPSDestLatitude',
243             Writable => 'rational64u',
244             Count => 3,
245             %coordConv,
246             PrintConvInv => 'Image::ExifTool::GPS::ToDegrees($val,undef,"lat")',
247             },
248             0x0015 => {
249             Name => 'GPSDestLongitudeRef',
250             Writable => 'string',
251             Count => 2,
252             PrintConv => { E => 'East', W => 'West' },
253             },
254             0x0016 => {
255             Name => 'GPSDestLongitude',
256             Writable => 'rational64u',
257             Count => 3,
258             %coordConv,
259             PrintConvInv => 'Image::ExifTool::GPS::ToDegrees($val,undef,"lon")',
260             },
261             0x0017 => {
262             Name => 'GPSDestBearingRef',
263             Writable => 'string',
264             Count => 2,
265             PrintConv => {
266             M => 'Magnetic North',
267             T => 'True North',
268             },
269             },
270             0x0018 => {
271             Name => 'GPSDestBearing',
272             Writable => 'rational64u',
273             },
274             0x0019 => {
275             Name => 'GPSDestDistanceRef',
276             Writable => 'string',
277             Count => 2,
278             PrintConv => {
279             K => 'Kilometers',
280             M => 'Miles',
281             N => 'Nautical Miles',
282             },
283             },
284             0x001a => {
285             Name => 'GPSDestDistance',
286             Writable => 'rational64u',
287             },
288             0x001b => {
289             Name => 'GPSProcessingMethod',
290             Writable => 'undef',
291             Notes => 'values of "GPS", "CELLID", "WLAN" or "MANUAL" by the EXIF spec.',
292             RawConv => 'Image::ExifTool::Exif::ConvertExifText($self,$val,1,$tag)',
293             RawConvInv => 'Image::ExifTool::Exif::EncodeExifText($self,$val)',
294             },
295             0x001c => {
296             Name => 'GPSAreaInformation',
297             Writable => 'undef',
298             RawConv => 'Image::ExifTool::Exif::ConvertExifText($self,$val,1,$tag)',
299             RawConvInv => 'Image::ExifTool::Exif::EncodeExifText($self,$val)',
300             },
301             0x001d => {
302             Name => 'GPSDateStamp',
303             Groups => { 2 => 'Time' },
304             Writable => 'string',
305             Format => 'undef', # (Casio EX-H20G uses "\0" instead of ":" as a separator)
306             Count => 11,
307             Shift => 'Time',
308             Notes => q{
309             when writing, time is stripped off if present, after adjusting date/time to
310             UTC if time includes a timezone. Format is YYYY:mm:dd
311             },
312             RawConv => '$val =~ s/\0+$//; $val',
313             ValueConv => 'Image::ExifTool::Exif::ExifDate($val)',
314             ValueConvInv => '$val',
315             # pull date out of any format date/time string
316             # (and adjust to UTC if this is a full date/time/timezone value)
317             PrintConvInv => q{
318             my $secs;
319             $val = $self->TimeNow() if lc($val) eq 'now';
320             if ($val =~ /[-+]/ and ($secs = Image::ExifTool::GetUnixTime($val, 1))) {
321             $val = Image::ExifTool::ConvertUnixTime($secs);
322             }
323             return $val =~ /(\d{4}).*?(\d{2}).*?(\d{2})/ ? "$1:$2:$3" : undef;
324             },
325             },
326             0x001e => {
327             Name => 'GPSDifferential',
328             Writable => 'int16u',
329             PrintConv => {
330             0 => 'No Correction',
331             1 => 'Differential Corrected',
332             },
333             },
334             0x001f => {
335             Name => 'GPSHPositioningError',
336             Description => 'GPS Horizontal Positioning Error',
337             PrintConv => '"$val m"',
338             PrintConvInv => '$val=~s/\s*m$//; $val',
339             Writable => 'rational64u',
340             },
341             # 0xea1c - Nokia Lumina 1020, Samsung GT-I8750, and other Windows 8
342             # phones write this (padding) in GPS IFD - PH
343             );
344              
345             # Composite GPS tags
346             %Image::ExifTool::GPS::Composite = (
347             GROUPS => { 2 => 'Location' },
348             GPSDateTime => {
349             Description => 'GPS Date/Time',
350             Groups => { 2 => 'Time' },
351             SubDoc => 1, # generate for all sub-documents
352             Require => {
353             0 => 'GPS:GPSDateStamp',
354             1 => 'GPS:GPSTimeStamp',
355             },
356             ValueConv => '"$val[0] $val[1]Z"',
357             PrintConv => '$self->ConvertDateTime($val)',
358             },
359             # Note: The following tags are used by other modules
360             # which must therefore require this module as necessary
361             GPSLatitude => {
362             SubDoc => 1, # generate for all sub-documents
363             Writable => 1,
364             Avoid => 1,
365             Priority => 1, # (necessary because Avoid sets default Priority to 0)
366             Require => {
367             0 => 'GPS:GPSLatitude',
368             1 => 'GPS:GPSLatitudeRef',
369             },
370             WriteAlso => {
371             'GPS:GPSLatitude' => '$val',
372             'GPS:GPSLatitudeRef' => '(defined $val and $val < 0) ? "S" : "N"',
373             },
374             ValueConv => '$val[1] =~ /^S/i ? -$val[0] : $val[0]',
375             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")',
376             PrintConvInv => 'Image::ExifTool::GPS::ToDegrees($val, 1, "lat")',
377             },
378             GPSLongitude => {
379             SubDoc => 1, # generate for all sub-documents
380             Writable => 1,
381             Avoid => 1,
382             Priority => 1,
383             Require => {
384             0 => 'GPS:GPSLongitude',
385             1 => 'GPS:GPSLongitudeRef',
386             },
387             WriteAlso => {
388             'GPS:GPSLongitude' => '$val',
389             'GPS:GPSLongitudeRef' => '(defined $val and $val < 0) ? "W" : "E"',
390             },
391             Require => {
392             0 => 'GPS:GPSLongitude',
393             1 => 'GPS:GPSLongitudeRef',
394             },
395             ValueConv => '$val[1] =~ /^W/i ? -$val[0] : $val[0]',
396             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "E")',
397             PrintConvInv => 'Image::ExifTool::GPS::ToDegrees($val, 1, "lon")',
398             },
399             GPSAltitude => {
400             SubDoc => [1,3], # generate for sub-documents if Desire 1 or 3 has a chance to exist
401             Desire => {
402             0 => 'GPS:GPSAltitude',
403             1 => 'GPS:GPSAltitudeRef',
404             2 => 'XMP:GPSAltitude',
405             3 => 'XMP:GPSAltitudeRef',
406             },
407             # Require either GPS:GPSAltitudeRef or XMP:GPSAltitudeRef
408             RawConv => '(defined $val[1] or defined $val[3]) ? $val : undef',
409             ValueConv => q{
410             my $alt = $val[0];
411             $alt = $val[2] unless defined $alt;
412             return undef unless defined $alt and IsFloat($alt);
413             return(($val[1] || $val[3]) ? -$alt : $alt);
414             },
415             PrintConv => q{
416             $val = int($val * 10) / 10;
417             return(($val =~ s/^-// ? "$val m Below" : "$val m Above") . " Sea Level");
418             },
419             },
420             GPSDestLatitude => {
421             Require => {
422             0 => 'GPS:GPSDestLatitude',
423             1 => 'GPS:GPSDestLatitudeRef',
424             },
425             ValueConv => '$val[1] =~ /^S/i ? -$val[0] : $val[0]',
426             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")',
427             },
428             GPSDestLongitude => {
429             SubDoc => 1, # generate for all sub-documents
430             Require => {
431             0 => 'GPS:GPSDestLongitude',
432             1 => 'GPS:GPSDestLongitudeRef',
433             },
434             ValueConv => '$val[1] =~ /^W/i ? -$val[0] : $val[0]',
435             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "E")',
436             },
437             );
438              
439             # add our composite tags
440             Image::ExifTool::AddCompositeTags('Image::ExifTool::GPS');
441              
442             #------------------------------------------------------------------------------
443             # Convert GPS timestamp value
444             # Inputs: 0) raw timestamp value string
445             # Returns: EXIF-formatted time string
446             sub ConvertTimeStamp($)
447             {
448 15     15 0 61 my $val = shift;
449 15         114 my ($h,$m,$s) = split ' ', $val;
450 15   100     141 my $f = (($h || 0) * 60 + ($m || 0)) * 60 + ($s || 0);
      50        
      100        
451 15         47 $h = int($f / 3600); $f -= $h * 3600;
  15         45  
452 15         41 $m = int($f / 60); $f -= $m * 60;
  15         41  
453 15         138 my $ss = sprintf('%012.9f', $f);
454 15 50       90 if ($ss >= 60) {
455 0         0 $ss = '00';
456 0 0       0 ++$m >= 60 and $m -= 60, ++$h;
457             } else {
458 15         114 $ss =~ s/\.?0+$//; # trim trailing zeros + decimal
459             }
460 15         174 return sprintf("%.2d:%.2d:%s",$h,$m,$ss);
461             }
462              
463             #------------------------------------------------------------------------------
464             # Print GPS timestamp
465             # Inputs: 0) EXIF-formatted time string
466             # Returns: time rounded to the nearest microsecond
467             sub PrintTimeStamp($)
468             {
469 15     15 0 66 my $val = shift;
470 15 100       180 return $val unless $val =~ s/:(\d{2}\.\d+)$//;
471 3         18 my $s = int($1 * 1000000 + 0.5) / 1000000;
472 3 50       14 $s = "0$s" if $s < 10;
473 3         39 return "${val}:$s";
474             }
475              
476             #------------------------------------------------------------------------------
477             # Convert degrees to DMS, or whatever the current settings are
478             # Inputs: 0) ExifTool reference, 1) Value in degrees,
479             # 2) format code (0=no format, 1=CoordFormat, 2=XMP format)
480             # 3) 'N' or 'E' if sign is significant and N/S/E/W should be added
481             # Returns: DMS string
482             sub ToDMS($$;$$)
483             {
484 99     99 0 345 my ($et, $val, $doPrintConv, $ref) = @_;
485 99         196 my ($fmt, @fmt, $num, $sign, $rtnVal);
486              
487 99 50       829 unless (length $val) {
488             # don't convert an empty value
489 0 0 0     0 return $val if $doPrintConv and $doPrintConv eq '1'; # avoid hiding existing tag when extracting
490 0         0 return undef; # avoid writing empty value
491             }
492 99 100       263 if ($ref) {
493 36 100       141 if ($val < 0) {
494 17         54 $val = -$val;
495 17         129 $ref = {N => 'S', E => 'W'}->{$ref};
496 17         56 $sign = '-';
497             } else {
498 19         61 $sign = '+';
499             }
500 36 100 66     223 $ref = " $ref" unless $doPrintConv and $doPrintConv eq '2';
501             } else {
502 63         276 $val = abs($val);
503 63         124 $ref = '';
504             }
505 99 100       248 if ($doPrintConv) {
506 68 100       185 if ($doPrintConv eq '1') {
507 51         187 $fmt = $et->Options('CoordFormat');
508 51 100       203 if (not $fmt) {
    100          
509 30         88 $fmt = q{%d deg %d' %.2f"} . $ref;
510             } elsif ($ref) {
511             # use signed value instead of reference direction if specified
512 10 50       69 $fmt =~ s/%\+/$sign%/g or $fmt .= $ref;
513             } else {
514 11         35 $fmt =~ s/%\+/%/g; # don't know sign, so don't print it
515             }
516             } else {
517 17         42 $fmt = "%d,%.8f$ref"; # use XMP format with 8 decimal minutes
518             }
519             # count (and capture) the format specifiers (max 3)
520 68         423 while ($fmt =~ /(%(%|[^%]*?[diouxXDOUeEfFgGcs]))/g) {
521 182 50       476 next if $1 eq '%%';
522 182         418 push @fmt, $1;
523 182 100       771 last if @fmt >= 3;
524             }
525 68         151 $num = scalar @fmt;
526             } else {
527 31         54 $num = 3;
528             }
529 99         170 my @c; # coordinates (D) or (D,M) or (D,M,S)
530 99         203 $c[0] = $val;
531 99 50       252 if ($num > 1) {
532 99         213 $c[0] = int($c[0]);
533 99         264 $c[1] = ($val - $c[0]) * 60;
534 99 100       219 if ($num > 2) {
535 77         138 $c[1] = int($c[1]);
536 77         196 $c[2] = ($val - $c[0] - $c[1] / 60) * 3600;
537             }
538             # handle round-off errors to ensure minutes and seconds are
539             # less than 60 (eg. convert "72 59 60.00" to "73 0 0.00")
540 99 100       732 $c[-1] = $doPrintConv ? sprintf($fmt[-1], $c[-1]) : ($c[-1] . '');
541 99 50       473 if ($c[-1] >= 60) {
542 0         0 $c[-1] -= 60;
543 0 0 0     0 ($c[-2] += 1) >= 60 and $num > 2 and $c[-2] -= 60, $c[-3] += 1;
544             }
545             }
546 99 100       227 if ($doPrintConv) {
547 68         362 $rtnVal = sprintf($fmt, @c);
548             # trim trailing zeros in XMP
549 68 100       652 $rtnVal =~ s/(\d)0+$ref$/$1$ref/ if $doPrintConv eq '2';
550             } else {
551 31         135 $rtnVal = "@c$ref";
552             }
553 99         1029 return $rtnVal;
554             }
555              
556             #------------------------------------------------------------------------------
557             # Convert to decimal degrees
558             # Inputs: 0) a string containing 1-3 decimal numbers and any amount of other garbage
559             # 1) true if value should be negative if coordinate ends in 'S' or 'W',
560             # 2) 'lat' or 'lon' to extract lat or lon from GPSCoordinates string
561             # Returns: Coordinate in degrees, or '' on error
562             sub ToDegrees($;$$)
563             {
564 223     223 0 518 my ($val, $doSign, $coord) = @_;
565 223 50       785 return '' if $val =~ /\b(inf|undef)\b/; # ignore invalid values
566             # use only lat or lon part of combined GPSCoordinates inputs
567 223 50 33     476 if ($coord and ($coord eq 'lat' or $coord eq 'lon') and
      66        
      33        
568             # (two formatted coordinate values with cardinal directions, separated by a comma)
569             $val =~ /^(.*(?:N(?:orth)?|S(?:outh)?)),\s*(.*(?:E(?:ast)?|W(?:est)?))$/i)
570             {
571 0 0       0 $val = $coord eq 'lat' ? $1 : $2;
572             }
573             # extract decimal or floating point values out of any other garbage
574 223         1387 my ($d, $m, $s) = ($val =~ /((?:[+-]?)(?=\d|\.\d)\d*(?:\.\d*)?(?:[Ee][+-]\d+)?)/g);
575 223 50       534 return '' unless defined $d;
576 223   100     1166 my $deg = $d + (($m || 0) + ($s || 0)/60) / 60;
      100        
577             # make negative if S or W coordinate
578 223 100       982 $deg = -$deg if $doSign ? $val =~ /[^A-Z](S(outh)?|W(est)?)\s*$/i : $deg < 0;
    100          
579 223         999 return $deg;
580             }
581              
582              
583             1; #end
584              
585             __END__