#!/usr/bin/perl
#
# $Id: NMEAfields.pm,v 1.2 2005/09/02 14:51:20 ben Exp $
# Module for interpreting NMEA strings
# Ben Smith, S/V Mother of Perl
#
# This program is free software, You may copy or redistribute it under the
# same terms as Perl itself.
# 
#########################################################################

my $VERSION=sprintf("%d.%02d", q$Revision: 1.2 $ =~ /(\d+)\.(\d+)/);

=head1 NAME

NMEAfields -- read captured NMEA data strings and return the data fields
as part of a structure.

=head1 SYNOPSIS

Make an interpretor with new(), and use it to generate data objects.

 use NMEAfields;
 my $I = NMEAfields->new(); # one one interpreter needed
 my $line; 
 my $dataP;

 while($line = <STDIN>){
    $dataP = $I->Interp($line); # get a data object
    # do something with the $dataP
 }

=head1 DESCRIPTION

These classes and methods are used to create a common interface to NMEA data
streams, such as from GPS, Depth sounders, Roll-Pitch-Heave sensors, navigation
systems, etc. In this first edition, only GGA and DPT strings are interpreted.
As I have time, I (or others) can add what they find.

The interpreter separates the instrument code from the data type and 
interprets based on the data type. For example, C<$IIGGA> and C<$GPGGA> are
interpreted in the same way.

This module does NOT do sum checks on the data streams.

The data objects returned by Interp are of their own class, B<NMEAdata>,
with their own methods.

For conversion from Lat/Long to UTM, the NMEAdata methods 
use Geo::Coordinates::UTM.

For conversion from GPS time to UNIX time notation (different epoch),
the NMEAdata methods
use Time::Local.

=head2 Understood Devices

The following device codes (first two letters of the string) are understood
by NMEAfields:

=over 10

=item AI => 'Automatic Identification System (AIS)'

=item AP => 'Heading Track Controller (AutoPilot), Magnetic'

=item GP => 'GPS - Global Positioning System'

=item HC => 'Heading Calculation'

=item II => 'Integrated Instrument'

=item IN => 'Integrated Navigation'

=item PG => 'Proprietry Device'

=item SD => 'Sounder Depth'

=item VD => 'Doppler Velocity'

=back

=head2 Understood Elements (Sentence Types)

The following element codes (third, fourth, and fifth letters of the
string) are understood by NMEAfields:

=over 10

=item GGA => '3-dimension fix with time and accuracy'

		  COG => 'course over ground',
		  BOD => 'Course waypoint to waypoint',
		  BWC => 'bearing from fix to waypoint',
		  BWW => 'Course waypoint to waypoint',
		  DBS => 'depth below surface',
		  DBT => 'depth below transducer',
		  DPT => 'depth',
		  GLL => 'geographic Position - Latitude/Longitude',
		  GSA => 'GPS DOP and active satellites',
		  GSV => 'satellites in view',
		  HDG => 'heading magnetic',
		  HDM => 'heading magnetic',
		  HDT => 'heading true',
		  MTW => 'Water temperature',
		  RMA => 'Recommended Minimum Specific Loran-C Data',
		  RMB => 'Recommended Minimum Navigation Information',
		  RMC => 'Recommended Minimum Specific GNSS Data',
		  RMZ => 'Altitude (Garmin)',
		  RME => 'Estimated Error Information (Garmin)',
		  RMF => 'GPS Fix Data (Garmin)',
		  RTE => 'Routes RTE - Routes',
		  VHW => 'Velocity through water',
		  VLW => 'Distance traveled through water',
		  VWR => 'Relative velocity of wind',
		  VTG => 'track made good and ground speed',
		  WPL => 'waypoint position',
		  XTE => 'cross track error',
		  ZDE => 'date and time',
		  ZDA => 'date and time',

=back

=head2 C<NMEAfields> Methods

=cut

###################### GLOBALS to module ####################

my $ellipsoid = 'WGS-84';

# the list of known devices
my %devices = (
	       AI => 'Automatice Identification System (AIS)',
	       AP => 'Heading Track Controller (AutoPilot), Magnetic',
	       GP => 'GPS - Global Positioning System',
	       HC => 'Heading Calculation',
	       II => 'Integrated Instrument',
	       IN => 'Integrated Navigation',
	       PG => 'Proprietry Device',
	       SD => 'Sounder Depth',
	       VD => 'Doppler Velocity',
	       );

# the list of known sentences
my %elements = (
		  GGA => '3-dimension fix with time and accuracy',
		  COG => 'course over ground',
		  BOD => 'Course waypoint to waypoint',
		  BWC => 'bearing from fix to waypoint',
		  BWW => 'Course waypoint to waypoint',
		  DBS => 'depth below surface',
		  DBT => 'depth below transducer',
		  DPT => 'depth',
		  GLL => 'geographic Position - Latitude/Longitude',
		  GSA => 'GPS DOP and active satellites',
		  GSV => 'satellites in view',
		  HDG => 'heading magnetic',
		  HDM => 'heading magnetic',
		  HDT => 'heading true',
		  MTW => 'Water temperature',
		  RMA => 'Recommended Minimum Specific Loran-C Data',
		  RMB => 'Recommended Minimum Navigation Information',
		  RMC => 'Recommended Minimum Specific GNSS Data',
		  RMZ => 'Altitude (Garmin)',
		  RME => 'Estimated Error Information (Garmin)',
		  RMF => 'GPS Fix Data (Garmin)',
		  RTE => 'Routes RTE - Routes',
		  VHW => 'Velocity through water',
		  VLW => 'Distance traveled through water',
		  VWR => 'Relative velocity of wind',
		  VTG => 'track made good and ground speed',
		  WPL => 'waypoint position',
		  XTE => 'cross track error',
		  ZDE => 'date and time',
		  ZDA => 'date and time',
		  );

############################# NMEAfields

package NMEAfields;

require Exporter;

@ISA = qw(Exporter);

@EXPORT = qw(
	     new
	     Interp
	     String
	     SetError
	     ClrError
	     Error
	     Checksum
	     );

############# new

=head3 C<NMEAfields-E<gt>new($GPSmaker)>

Creates a new interpreter for NMEA streams. The NMEA strings are not actually 
proprietary information of the equipment manufacturers, and so vary from
one manufacturer to another. Many are not publicly documented.

I<Takes an option argument for the GPS manufacturer. By default, GARMIN>

=cut

sub new {
    my $class = shift;
    my $gpsMaker = shift;
    

    my $self = {
	gpsMaker => 'GARMIN', # default mfgr
	string => '',  # will hold the string for Interp
	device => '',  # first two characters of device-string
	element => '', # last three characters of device-string
	data => [] ,   # list of data fields
	fields => {},   # hash of data fields with field names if known
	ERROR => undef,
    };

    if($gpsMaker) { 
	$gpsMaker =~ tr/a-z/A-Z/; # all uppercase
	$self->{gpsMaker} = $gpsMaker;
    }

    return bless $self, $class;
}

############# Checksum
=head3 C<NMEAfields-E<gt>Checksum()>
Returns 1 (True) if the calculated checksum equals the transmitted (embedded) 
checksum. Returns 0 (False) if they do not agree. If FALSE, then an error
is placed in the Error attribute. See NMEA-<gt>Error.
=cut

sub Checksum {
    my $self = shift;
    if( $self->{CKS} eq $self->{calcCKS}) {
	return 1;
    } else {
	$self->SetError(
	"Checksum error: calcualte=$self->{calcCKS} provided=$self->{CKS}"
			);
	return 0;
    }
}

############ SetError
=head3 C<NMEAdata-E<gt>SetError(string)>
Sets the Error attribute to string.
=cut

sub SetError {
    my $self = shift;
    my $error =shift;
    $self->{ERROR} =    "!Error: \t$error\n!String:\t $self->{string}\n";
}

############# ClrError
=head3 C<NMEAfields-E<gt>ClrError()>
Clears the Error attribute to undef.
=cut

sub ClrError {
    my $self = shift;
    $self->{ERROR} = undef;
}

############ Error
=head3 C<NMEAfields-E<gt>Error()>
Clears the Error attribute to undef and
returns any error string that it was
set to.
=cut

sub Error {
    my $self = shift;
    my $errorStr = $self->{ERROR};
    $self->ClrError();
    return $errorStr;
}

sub DeviceCode {
  my $self = shift;
  return $self->{device};
}

sub Device {
  my $self = shift;
  return (defined $devices{$self->{device}}) ? 
      $devices{$self->{device}} :
      '';
}

sub ElementCode {
  my $self = shift;
  return $self->{element};
}

sub Element {
  my $self = shift;
  return $elements{$self->{element}};
}


###### Interp 

=head3 C<NMEAfields-E<gt>Interp($string)>

This method takes the NMEA string as its argument. It returns a new
object of the class B<NMEAdata>, which has its own set of methods.
C<Interp> will strip leading and trailing whitespace before parsing
the string.

If the argument string is empty or can not be interpreted as NMEA, C<Interp>
will return C<under>.

I<It takes a string as its argument>

=cut


sub Interp { # interprets the NMEA strings
    my $self = shift;
    my $string = shift;
    
    $self->Clear();
    # remove leading and trailing whitespace
    $string =~ s/^\s+//;
    $string =~ s/\s+$//;

    return undef if $string !~ /^\$/; # not a valid NMEA string 
  
    $self->{string} = $string; # for debugging mostly

    # pull off checksum value if it is included
    if($string =~ s/\*(.+)$//) {
	$self->{CKS} = $1;
    }
    $self->calcChecksum();

    # break remainder into comma delimited fields, the first being the prefix
    my($prefix, @fields) = split(/,/,$string);
    return undef if not $prefix;

    
    $self->{device} = substr($prefix,1,2);
    $self->{element} = substr($prefix,3,3);
    $self->{data} = [@fields];

    # Let's see what we can recognize
    my $element = $self->{element};
    if(grep /$element/, keys %elements) {
	eval("\$self->$element()"); # run the corresponding method
	$self->Cook(); # process those fields that can be cooked
	return NMEAdata->new($self); # return a new fields class object
    } else {
	return undef;
    }
}

sub String {
    my $self = shift;
    return $self->{string};
}

################ Clear

# private method used by Interp in preparation for evaluating 
# a new string


sub Clear { # clears the fields that are used or Interp
    my $self = shift;

    $self->{string}  = '' ;
    $self->{device}  = '' ;
    $self->{element} = '' ;
    $self->{data}    = [] ;
    $self->{fields}  = {};
}


############### mapData ###################

sub mapData {
    my $self = shift;
    my $fldListPtr = shift;
    my $at = shift;

    $at = 0 unless $startAt; # default value

    for my $field (@$fldListPtr) {
	$self->{fields}->{$field} = $self->{data}[$at];
	++ $at;
    }

}

################### calcChecksum

sub calcChecksum {
    my $self = shift;
    # delete leading $ and existing checksum
    my $stringToCheck = $self->{string};
    $stringToCheck =~ s/(^\$|\*.*$)//g;
    my $sum = 0;
	foreach(split(//,$stringToCheck)){
	    $sum ^= ord($_); # XOR sum
	}
     # generate the string with alphanumeric in uppercase
     $self->{calcCKS} = unpack('H*',pack("C",$sum));
     $self->{calcCKS} =~ tr/a-z/A-Z/;
}



############################ the element field readers


#### BOD

sub BOD { # Course waypoint to waypoint
    my $self = shift;
    my $fieldsRef = [qw(
			BearingTrue  
			True         
			BearingMagn  
			Magn         
			GoalWP_ID    
			BeginingWP_ID
			)];
    $self->mapData($fieldsRef);
}

#### BWC

sub BWC { # Bearing to waypoint
    my $self = shift;
    my $fieldsRef = [qw(
			RawFixTime     
			WPRawLat       
			WPNorthSouth   
			WPRawLong      
			WPEastWest     
			Bearing2wpTrue
			Bearing2wpMagn
			Distance2wpNM 
			WP_ID          
			)];
    $self->mapData($fieldsRef);
}

##### BWW

sub BWW { # Course waypoint to waypoint
    my $self = shift;
    my $fieldsRef = [qw(
			BearingTrue  
			True         
			BearingMagn  
			Magn         
			GoalWP_ID    
			BeginingWP_ID
			)];
    $self->mapData($fieldsRef);
}

#### COG

sub COG {
    my $self = shift;
    my $fieldsRef = [qw(
			RawFixTime  
			RawLat      
			NorthSouth  
			RawLong     
			EastWest    
			SOG         
			COG         
			)];
    $self->mapData($fieldsRef,0);
    if($self->{gpsMaker} eq 'LOWRANCE') {
	$fieldsRef = [qw(
			 RawFixDate
			 TrueMagn
			 )];
	$self->mapData($fieldsRef,7); # Lowrance has these extra 2 fields
    }

} 

##### DBS

sub DBS { # Depth Below Surface 
    my $self = shift;
    my $fieldsRef = [qw(
			DBSfeet
			DBSf   
			DBSmeters  
			DBSM       
			DBSfathoms 
			DBSF       
			)];
    $self->mapData($fieldsRef);
}

##### DBT

sub DBT { # Depth Below Transducer 
    my $self = shift;
    my $fieldsRef = [qw(
			DBTfeet   
			DBTf      
			DBTmeters 
			DBTM      
			DBTfathoms
			DBTF      
			)];
    $self->mapData($fieldsRef);
}

##### DPT

sub DPT { # Depth 
    my $self = shift;
    my $fieldsRef = [qw(
			DBTmeters 
			OffsetMeters
			MaxRange
			)];
    $self->mapData($fieldsRef);
}

#### GGA

sub GGA {
    my $self = shift;
    my $fieldsRef = [qw(
			RawFixTime 
			RawLat     
			NorthSouth 
			RawLong    
			EastWest   
			)];
    $self->mapData($fieldsRef);
    if($self->{gpsMaker} eq 'GARMIN') {
	$fieldsRef = [qw(
			 TypeFix
			 HDOP
			 Altitude
			 GeodSeparation
			 SeparationUnits
			 )];
	$self->mapData($fieldsRef,5);
    }elsif($self->{gpsMaker} eq 'LOWRANCE') {
	$fieldsRef = [qw(
			 SOG
			 COG
			 RawFixDate
			 TrueMagn
			 )];
	$self->mapData($fieldsRef,5);
    }
}

##### GLL Geographic Position - Latitude/Longitude

sub GLL {
    my $self = shift;
    my $fieldsRef = [qw(
			RawLat
			NorthSouth
			RawLong
			EastWest
			RawFixTime
			Status
			Mode
			)];
    $self->mapData($fieldsRef);
}
			

##### GSA

sub GSA { # Active Satellites
    my $self = shift;
    my $fieldsRef = [qw(
			AutoManual
			fix3D     
			PRN_01    
			PRN_02    
			PRN_03    
			PRN_04    
			PRN_05    
			PRN_06    
			PRN_07    
			PRN_08    
			PRN_09    
			PRN_10    
			PRN_11    
			PRN_12    
			PDOP      
			HDOP      
			VDOP      
			)];
    $self->mapData($fieldsRef);
}

##### GSV

sub GSV { # Satellites in view - a multi-sentence message
    my $self = shift;
    my $fieldsRef = [qw(
			NumberMessages   
			SequenceNumber   
			SatellitesInView 
			Satellite_ID_1   
			Elevation_1      
			Azimuth_1        
			SRN_1            
			Satellite_ID_2   
			Elevation_2      
			Azimuth_2        
			SRN_2            
			Satellite_ID_3   
			Elevation_3      
			Azimuth_3        
			SRN_3            
			Satellite_ID_4   
			Elevation_4      
			Azimuth_4        
			SRN_4            
			)];
    $self->mapData($fieldsRef);
}

##### HDG

sub HDG { # Heading, Deviation, Variation
    my $self = shift;
    my $fieldsRef = [qw(
			MagneticHeading
			)];
    $self->mapData($fieldsRef);
}

##### HDM 

sub HDM { # Heading Magnetic
    my $self = shift;
    my $fieldsRef = [qw(
			MagneticHeading 
			HDMM
			)];
    $self->mapData($fieldsRef);
}

##### HDT 

sub HDT { # Heading True
    my $self = shift;
    my $fieldsRef = [qw(
			TrueHeading 
			HDTT        
			)];
    $self->mapData($fieldsRef);
}

##### MTW 

sub MTW { # Water Tempurature
    my $self = shift;
    my $fieldsRef = [qw(
			WaterTemp
			)];
    $self->mapData($fieldsRef);
}

##### RMA

sub RMA { # Recommended Minimum Specific Cloran-C Data
    my $self = shift;
    my $fieldsRef = [qw(
			Status
			RawLat          
			NorthSouth      
			RawLong         
			EastWest        
			TimeDiffAuS
			TimeDiffBus
			SOGk
			COGt
			MagnVariation   
			VarEastWest 
			Mode
			)];
    $self->mapData($fieldsRef);
}


##### RMB

sub RMB { # Recommended Minimum Navigation Information
    my $self = shift;
    my $fieldsRef = [qw(
			Status
			XTAnm
			DirToSteer
			OrigWayptID
			DestWayptID
			DestRawLat
			DestNorthSouth      
			DestRawLong
			DestEastWest        
			Range
			Bearing
			VMGkt
			Status
			Mode
			)];
    $self->mapData($fieldsRef);
}

##### RMC

sub RMC { # Recommended minimum Transit Data
    my $self = shift;
    my $fieldsRef = [qw(
			RawFixTime      
			ActiveVoid      
			RawLat          
			NorthSouth      
			RawLong         
			EastWest        
			SOG             
			COG             
			RawFixDate      
			MagnVariation   
			VarEastWest     
			)];
    $self->mapData($fieldsRef);
}

##### RMZ  Altitude (Garmin) 

sub RMZ { 
    my $self = shift;
    my $fieldsRef = [qw(
			Altitude
			Unit
			PosFixDim
			)];
    $self->mapData($fieldsRef);
}


##### RME 'Estimated Error Information (Garmin)',

sub RME { 
    my $self = shift;
    my $fieldsRef = [qw(
			EstHorizErrMeters
			EstVertErrMeters
			EPE-EstPosErrMeters
			)];
    $self->mapData($fieldsRef);
}

##### RMF 'GPS Fix Data (Garmin)',

sub RMF { 
    my $self = shift;
    my $fieldsRef = [qw(
			GPSweek
			GPSseconds
			RawFixDate
			RawFixTime
			GPSleapSeconds
			RawLat          
			NorthSouth      
			RawLong         
			EastWest        
			FixType
			SOG             
			COG             
			PositionDilutionPrecision
			TimeDilutionPrecision
			)];
    $self->mapData($fieldsRef);
}




##### RTE

sub RTE { # Routes RTE - Routes
    my $self = shift;
    my $fieldsRef = [qw(
			TotalMssgsXmt
			MessageNum
			MessageMode
			RouteID
			WayptID[0]
			)];
    $self->mapData($fieldsRef);
    # add additional Waypt IDs
    # hmmm!
}


##### VLW

sub VLW { # Distance Through Water (very sketchy)
    my $self = shift;
    my $fieldsRef = [qw(
			TotalCumulativeDistance
			Nm1
			DistanceSinceReset
			Nm2
			)];
    $self->mapData($fieldsRef);
}

##### VHW

sub VHW { # Water Speed (this one is sketchy)
    my $self = shift;
    my $fieldsRef = [qw(
			HeadingTrue
			True
			HeadingMagn
			Magn
			WaterSpeedKts
			WaterSpeedKPH
			)];
    $self->mapData($fieldsRef);
}

##### VTG

sub VTG { # Track Made Good & Ground Speed
    my $self = shift;
    my $fieldsRef = [qw(
			COG_true   
			VTG_T      
			COG_magn   
			COG_M      
			SOG_knots  
			SOG_N      
			SOG_kph    
			SOG_K      
			)];
    $self->mapData($fieldsRef);
}

##### VWR

sub VWR { # Relative Wind Speed
    my $self = shift;
    my $fieldsRef = [qw(
			RelativeAngle
			LRofBow
			WindSpeedKts
			WindSpeedMPS
			WindSpeedKPH
			)];
    $self->mapData($fieldsRef);
}

##### WPL

sub WPL { # Waypoint position
    my $self = shift;
    my $fieldsRef = [qw(
			RawLat     
			NorthSouth 
			RawLong    
			EastWest   
			WP_Name    
			)];
    $self->mapData($fieldsRef);
}


##### XTE

sub XTE { # Cross Track Error 
    my $self = shift;
    my $fieldsRef = [qw(
			WarningFlag   
			LoranLockFlag 
			XTE_distance  
			XTE_LeftRight 
			XTE_units     
			)];
    $self->mapData($fieldsRef);
}

##### ZDA

sub ZDA { # Date and Time for Synchronization
  my $self = shift;
    my $fieldsRef = [qw(
			RawFixTime    
			Day
			Month
			Year
			LocalZoneHours
			LocalZoneMins 
			)];
    $self->mapData($fieldsRef);
}

##### ZDE

sub ZDE { # Fix Date and Time
  my $self = shift;
    my $fieldsRef = [qw(
			RawFixTime    
			RawFixDate    
			LocalZoneHours
			LocalZoneMins 
			)];
    $self->mapData($fieldsRef);
}


################### Cook

sub Cook {
    my $self = shift;
    if(defined $self->{fields}{RawFixTime}) {
	($self->{fields}{FixTimeH},
	 $self->{fields}{FixTimeM},
	 $self->{fields}{FixTimeS}) =
	     $self->CookTime($self->{fields}{RawFixTime});
    }
    if(defined $self->{fields}{RawFixDate}) {
	($self->{fields}{FixDateD},
	 $self->{fields}{FixDateM},
	 $self->{fields}{FixDateY}) =
	     $self->CookDate($self->{fields}{RawFixDate});
    }
    if(defined $self->{fields}{RawLat}) {
	$self->{fields}{LatDecDeg} =
	    $self->CookLatitude(
				$self->{fields}{RawLat},
				$self->{fields}{NorthSouth}
				);
    }
    if(defined $self->{fields}{RawLong}) {
	$self->{fields}{LongDecDeg} =
	    $self->CookLongitude(
				 $self->{fields}{RawLong},
				 $self->{fields}{EastWest}
				 );
    }
    if(defined $self->{fields}{WPRawLat}) {
	$self->{fields}{WPLatDecDeg} =
	    $self->CookLatitude(
				$self->{fields}{WPRawLat},
				$self->{fields}{WPNorthSouth}
				);
    }
    if(defined $self->{fields}{WPRawLong}) {
	$self->{fields}{WPLongDecDeg} =
	    $self->CookLongitude(
				 $self->{fields}{WPRawLong},
				 $self->{fields}{WPEastWest}
				 );
    }
}

sub CookTime { # break raw fields into sensible values
    my $self = shift;
    my $rawTime = shift;
    return(
	   substr($rawTime,0,2), # hour
	   substr($rawTime,2,2), # minute
	   substr($rawTime,4));  # second
}

sub CookDate {
    my $self = shift;
    my $rawDate = shift;
    return (
	substr($rawDate,0,2), # day
	substr($rawDate,2,2), # month
	substr($rawDate,4));  # year
}

sub CookLatitude {
    my $self = shift;
    my $rawLat = shift;
    my $NS = shift;
    my $LatDecDeg =
	    substr($rawLat,0,2) +
	    substr($rawLat,2) / 60;
    $LatDecDeg *= ($NS eq 'S') ? -1 : 1;
    return $LatDecDeg;
}

sub CookLongitude {
    my $self = shift;
    my $rawLong = shift;
    my $EW = shift;
    my $LongDecDeg =
	    substr($rawLong,0,3) +
	    substr($rawLong,3) / 60;
    $LongDecDeg *= ($EW eq 'W') ? -1 : 1;
    return $LongDecDeg;

}



#####################################################################
######################### NMEAdata ##################################

package NMEAdata;
use Geo::Coordinates::UTM;
use Time::Local;
require Exporter;
@ISA = qw(Exporter);
@EXPORT = qw(
	     new
	     Data
	     Cooked
	     All
	     Checksum
	     GetFields
	     DeviceCode
	     ElementCode
	     Element
	     PositionStr
	     TimeStr
	     LL2UTM
	     Zone
	     Easting
	     Northing
	     UNIXtime
	     Checksum
	     SetError
	     ClrError
	     Error
	     );


# the field lists == these lists are a pain to maintain. Maybe we should
#          have the users add what they want in these lists
#          and then they can customize them the way they wish

my @rawfields = qw(RawFixTime 
		    RawFixDate
		    RawLat 
		    NorthSouth
		    RawLong
		    EastWest
		    WPRawLat
		    WPNorthSouth
		    WPRawLong
		    WPNorthSouth
		    TrueMagn
		    COG
		    SOGunit
		    SOG
		    dptDepthFeet
		    dptDepthMeters
		    dptDepthFathoms
		    );

my @cookedfields = qw(FixTimeH 
		    FixTimeM 
		    FixTimeS 
		    FixDateD
		    FixDateM
		    FixDateY
		    LatDecDeg
		    LongDecDeg
		    COG
		    SOGunit
		    SOG
		    TrueMagn
		    dptDepthFeet
		    dptDepthMeters
		    dptDepthFathoms
		    );
		    

############## new
############ a method that is used by only the NMEAfields->Interp
sub new {
    my $class = shift;
    my $NMEAfields = shift;

    my $self =  $NMEAfields->{fields} ;

    # add on the device and element info
    $self->{_DEVICE_} = $NMEAfields->{device};
    $self->{_ELEMENT_} = $NMEAfields->{element};
    $self->{_CKS_} = $NMEAfields->{CKS};
    $self->{_calcCKS_} = $NMEAfields->{calcCKS};
    $self->{_STRING_} = $NMEAfields->{string};
    $self->{_ERROR_} = $NMEAfields->{ERROR};
    
    return bless $self, $class;
}

############ field list output methods

=head2 C<NMEAdata> Methods

=cut

############## Raw

=head3 C<NMEAdata-E<gt>Raw>

C<Raw> returns a subset of the total C<NMEAdata> structure. 
These fields are not particularly useful to outside programs. 
The main reason for the C<NMEAfields::> module is to produce data
that doesn t require any interpretation outside of the module.

The subset has any of the available following fields:

=over 4

=item C<RawFixTime> 

The general format of time is C<hhmmss.sss>

=item C<RawFixDate> 

The general format of time is C<ddmmyy>

=item C<RawLat> 

The general format is C<ddmm.mmmm>

=item C<NorthSouth>

A character C<N> or C<S> designating hemisphere of latitude

=item C<RawLong>

The general format is C<ddmm.mmmm>

=item C<EastWest>

A character C<E> or C<W> designating hemisphere of longitude

=item C<COG>

Course over ground (True if not otherwise specified by C<TrueMagn>

=item C<TrueMagn>

A character C<T> or C<M> designating type of bearing/course. Assume
True (rather than Magnetic) if this field is missing.

=item C<SOG>

Speed over ground. Assume knots if not specified by a C<SOGunit>

=item C<SOGunit>

A character C<N> (knots), C<K> (kilometers per hour), C<M> (miles per hour).
Assume knots if this field is missing.

=item C<dptDepthFeet>

Depth below transducer in feet.

=item C<dptDepthMeters>

Depth below transducer in meters.

=item C<dptDepthFathoms>

Depth below transducer in fathoms.

=back

These values are in the NMEA native format which is usually readable, but
often a munged up version of the data. 

=cut

sub Raw {
    my $self = shift;
    
    return $self->GetFields(@rawfields);
}

############## Cooked

=head3 C<NMEAdata-E<gt>Cooked>

C<Cooked> returns a subset of the total C<NMEAdata> structure. 
These fields are the more useful versions of the data that comes
from the raw fields. But, it doesn t stop here, there are many
other methods that give specific formats and transformations of the
data. (See Below.)

The subset has any of the available following fields:

=over 4

=item C<FixTimeH> 

The hour

=item C<FixTimeM> 

The minute

=item C<FixTimeS>

The second

=item C<FixDateD>

The day of the month

=item C<FixDateM>

The month of the year (January = 1)

=item C<FixDateY>

The year (only the last two digits)

=item C<LatDecDeg>

The latitude in signed decimal degrees. Negative latitudes are b<S>outh
of the equator.

=item C<LongDecDeg>

The longitude in signed decimal degrees. Negative longitudes are b<W>est
of the prime meridian.

=item C<COG>

Course over ground (True if not otherwise specified by C<TrueMagn>
(Unchanged from the C<Raw> method.)

=item C<TrueMagn>

A character C<T> or C<M> designating type of bearing/course. Assume
True (rather than Magnetic) if this field is missing.
(Unchanged from the C<Raw> method.)

=item C<SOG>

Speed over ground. Assume knots if not specified by a C<SOGunit>
(Unchanged from the C<Raw> method.)

=item C<SOGunit>

A character C<N> (knots), C<K> (kilometers per hour), C<M> (miles per hour).
Assume knots if this field is missing.
(Unchanged from the C<Raw> method.)

=item C<dptDepthFeet>

Depth below transducer in feet.
(Unchanged from the C<Raw> method.)

=item C<dptDepthMeters>

Depth below transducer in meters.
(Unchanged from the C<Raw> method.)

=item C<dptDepthFathoms>

Depth below transducer in fathoms.
(Unchanged from the C<Raw> method.)

=back

These values are in the NMEA native format which is usually readable, but
often a munged up version of the data. 

=cut

sub Cooked {
    my $self = shift;
    
    return $self->GetFields(@cookedfields);
}

############ All
sub All {
    my $self = shift;
    my	@All = keys %$self;

    return $self->GetFields(@All);
}
    # process some of the formats into more common forms



############### GetFields -- gets fields from a specified struct base

# a private method used by other methods in this class

sub GetFields {
    my $self = shift;
    my @fields = @_; # the rest of the arguments
    my $field;
    my $result = {};
    

    foreach $field (@fields) {
	next if $field =~ /^_\S_$/;  # ignore special fields
	if(defined $self->{$field}) {
	    $result->{$field} = $self->{$field};
	}
    }
    bless $result;
    return $result;
}

################### Data ##############################################
# a general data retrieval routine which returns a list of data
# corresponding to the list of field names. This can be applied
# to any dataset.

=head3 C<NMEAdata-E<gt>Data(fieldList)>
Returns the list of data values corresponding to the fields
in the fieldlist. A single field name returns a single value
provided that that data exists.
=cut 

sub Data {
    my $self = shift;
    my @fieldlist = @_;
    my @results = ();

    for(@fieldlist){
	push(@results,$self->{$_});
    }
    return @results;
}



################### Special Data and Formats ##########################

############### Checksum
=head3 C<NMEAdata-E<gt>Checksum()>
Returns 1 (True) if the calculated checksum equals the transmitted (embedded) 
checksum. Returns 0 (False) if they do not agree. If FALSE, then an error
is placed in the Error attribute. See NMEA-<gt>Error.
=cut

sub Checksum {
    my $self = shift;
    if( $self->{_CKS_} eq $self->{_calcCKS_}) {
	return 1;
    } else {
	$self->SetError(
	"Checksum error: calcualte=$self->{_calcCKS_} provided=$self->{_CKS_}"
			);
	return 0;
    }
}


############## SetError
=head3 C<NMEAdata-E<gt>SetError(string)>
Sets the Error attribute to string.
=cut

sub SetError {
    my $self = shift;
    my $error = shift;
    $self->{_ERROR_} =    "!Error: \t$error\n!String:\t $self->{string}\n";
}

=head3 C<NMEAdata-E<gt>ClrError(string)>
Clears the Error attribute to undef.
=cut

sub ClrError {
    my $self = shift;
    $self->{_ERROR_} = undef;
}

=head3 C<NMEAdata-E<gt>Error(string)>
Clears the Error attribute to undef and
returns any error string that it was
set to.
=cut

sub Error {
    my $self = shift;
    my $errorStr = $self->{_ERROR_};
    $self->ClrError();
    return $errorStr;
}

sub DeviceCode {
  my $self = shift;
  return $self->{_DEVICE_};
}

sub Device {
  my $self = shift;
  return (defined $devices{$self->{_DEVICE_}}) ? 
      $devices{$self->{_DEVICE_}} :
      '';
}

sub ElementCode {
  my $self = shift;
  return $self->{_ELEMENT_};
}

sub Element {
  my $self = shift;
  return $elements{$self->{_ELEMENT_}};
}


################ PositionStr 
sub PositionStr { # just a data formatting method
    my $self = shift;

    return sprintf("%08.5f, %08.5f",
		   $self->{LatDecDeg},
		   $self->{LongDecDeg});
}


################ TimeStr 

=head3 C<NMEAdata-E<gt>TimeStr>

Returns a zero padded string of the format C<hh:mm:ss MM/DD/YY> for
the time of the fix

=cut 

sub TimeStr { # another formatting method
    my $self = shift;
    
    return undef unless defined $self->{FixTimeH};

    return sprintf("%02d:%02d:%05.3f %02d/%02d/%02d",
		   $self->{FixTimeH},
		   $self->{FixTimeM},
		   $self->{FixTimeS},
		   $self->{FixDateM},
		   $self->{FixDateD},
		   $self->{FixDateY});
}

################ LL2UTM

=head3 C<NMEAdata-E<gt>LL2UTM>

Returns the three element list C<($UTMzone,$Easting,$Northing)>. The
values of easting and northing are in meters. The UTM projection facilitates
plotting because it represents the surface of the planet as a rectiliniar grid.
The projection is based on the WGS84 Ellipsoid, the standard for most GPS 
fixes. 

Uses the module C<Geo::Coordinates::UTM>

=cut 

sub LL2UTM { # converts from Lat, Long (and ellipsoid) to UTM 
    my $self = shift; #                  zone, easting, and northing
    ($self->{zone},
	$self->{easting},
	$self->{northing}) = 
	latlon_to_utm($ellipsoid,
		      $self->{LatDecDeg},
		      $self->{LongDecDeg});
    return($self->{zone},
	   $self->{easting},
	   $self->{northing});
}

############# Zone

=head3 C<NMEAdata-E<gt>Zone>

Returns the UTM Zone. For a better understand of UTM zones see
C<Geo::Coordinates::UTM>

=cut 

sub Zone {
    my $self = shift;
    
    # run the conversion if it hasn't already been done
    $self->LL2UTM() unless defined $self->{zone};
    return $self->{zone};
}

############# Easting

=head3 C<NMEAdata-E<gt>Easting>

Returns the easting in meters. See C<LL2UTM> for information
on zones, easting, and northing.

=cut 


sub Easting {
    my $self = shift;
    
    # run the conversion if it hasn't already been done
    $self->LL2UTM() unless defined $self->{easting};
    return $self->{easting};
}

############## Northing

=head3 C<NMEAdata-E<gt>Northing>

Returns the easting in meters. See C<LL2UTM> for information
on zones, easting, and northing.

=cut 


sub Northing {
    my $self = shift;
    
    # run the conversion if it hasn't already been done
    $self->LL2UTM() unless defined $self->{northing};
    return $self->{northing};
}

############### UNIXtime

=head3 C<NMEAdata-E<gt>UNIXtime>

Returns the long-integer/float value of the fixtime, using the UNIX
style epoch of January 1, 1970. This is a convenient value
for timestamping. If the device returns an fractional second
in its time string, the floating point version is returned.

This uses the module C<Time::Local>

=cut 

sub UNIXtime {
    my $self = shift;
    if(($self->{RawFixTime} =~ /^\d+$/) && ($self->{RawFixDate} =~ /^\d+$/)) {
	$self->{UNIXtime} = timegm(
				   $self->{FixTimeS},
				   $self->{FixTimeM},
				   $self->{FixTimeH},
				   $self->{FixDateD},
				   $self->{FixDateM}-1,
				   $self->{FixDateY}
				   ) unless defined $self->{UNIXtime};
    } else {
	$self->{UNIXtime} = undef;
    }
    return $self->{UNIXtime};
}

sub PrintDump {
    my $self = shift;
    
    for my $key (sort keys %$self) {
	next if $key =~ /^_\S+_$/;
	print "\t$key => $$self{$key}\n";
    }
}

1;

__END__

=head1 AUTHOR

Ben Smith, Captain of the Sailing Vessel Mother of Perl, in the Caribbean
Seas
=head1 BUGS

To be determined

=head1 SEE ALSO

=head1 COPYRIGHT

This program is free software, You may copy or redistribute it under the
same terms as Perl itself.

=cut

########################################################################
#
# $Log: NMEAfields.pm,v $
# Revision 1.2  2005/09/02 14:51:20  ben
# Added many new data types and device types.
# Added the Data method.
#
# Revision 1.1  2004/11/22 17:10:51  ben
# Initial revision
#
#
########################################################################
Home
 Boat
 Photos
 Videos
 Hydrography
   nmea2xyzt
   NMEAfields
   Datalogger
    Source
  Cleaning
  Analysis
  GridPlot
 Blog
 SiteMap