package Geo::Coordinates::WGS84toCH1903;

use strict;
use warnings;

our $VERSION = '0.01';

sub new {
	my $class = shift;	
	
	my $self = {	
		_WGSLatDec   => undef,
		_WGSLongDec  => undef,
		_CH1903X     => undef,        
		_CH1903Y     => undef	
	};

	bless($self, $class);
	return $self;
}

sub attach_WGS84_dec {

	# Read params
	my @params = @_;
	my ($self, $lat, $long) = @params;
	
	# Sanity check
	if (defined($lat) && defined($long) && (scalar(@params) == 3)){
		
		# Process if defined
		$self->{_WGSLatDec}  = $lat;
		$self->{_WGSLongDec} = $long;
	}
	else{
		# Give warning if not defined
		print "Did not attach: attach_WGS84_Dec needs a decimal latitude and longitude as input (2 values)!\n"
	}
}	

sub attach_WGS84_sex {

	# Read params
	my @params = @_;
	my ($self, $latD, $latM, $latS, $longD, $longM, $longS) = @params;
	
	# Sanity check
	if (defined($latD) && defined($latM) && defined($latS) && defined($longD) && defined($longM) && defined($longS) && (scalar(@params) == 7)){
		
		# Process if defined
		$self->{_WGSLatDec}  = SEXtoDEC($latD,$latM,$latS);
		$self->{_WGSLongDec} = SEXtoDEC($longD,$longM,$longS);
	}
	else{
		# Give warning if not defined
		print "Did not attach: attach_WGS84_Sex needs latitude and longitude DMS as input (6 values)!\n"
	}
}

sub attach_CH1903 {

	# Read params
	my @params = @_;
	my ($self, $y, $x) = @params;
	
	# Sanity check
	if (defined($x) && defined($y) && (scalar(@params) == 3)){
		
		# Process if defined
		$self->{_CH1903X}  = $x;
		$self->{_CH1903Y}  = $y;
	}
	else{
		# Give warning if not defined
		print "Did not attach: attach_CH1903 needs a Y and X coordinate!\n"
	}
}

sub fetch_CH1903 {
	
	# Read params
	my $self = shift;
	
	# Perform checks
	if (defined($self->{_CH1903X}) && defined($self->{_CH1903X})){
		
		# Report if already defined
		return($self->{_CH1903Y},$self->{_CH1903X});
	}
	elsif(defined($self->{_WGSLatDec}) && defined($self->{_WGSLongDec})){
	
		# Calculate if angle is defined
		$self->{_CH1903X} = int(WGStoCH1903x($self->{_WGSLatDec},$self->{_WGSLongDec}) + 0.5);
		$self->{_CH1903Y} = int(WGStoCH1903y($self->{_WGSLatDec},$self->{_WGSLongDec}) + 0.5);
		
		# Return
		return($self->{_CH1903Y},$self->{_CH1903X});
	}
	else{
	
		# Give warning if nothing attached
		print "Did not fetch CH1903 data: no data has been attached!\n";
	}
}

sub fetch_WGS84_dec {

	# Read params
	my $self = shift;
	
	# Perform checks
	if (defined($self->{_WGSLatDec}) && defined($self->{_WGSLongDec})){
	
		# Report if already defined
		return($self->{_WGSLatDec},$self->{_WGSLongDec});
	}
	elsif(defined($self->{_CH1903X}) && defined($self->{_CH1903X})){
		
		# Calculate if CH1903 is defined
		$self->{_WGSLatDec} = int((CH1903toWGSlat($self->{_CH1903Y},$self->{_CH1903X}))*10000 + 0.5)/10000;
		$self->{_WGSLongDec}= int((CH1903toWGSlong($self->{_CH1903Y},$self->{_CH1903X}))*10000 + 0.5)/10000;
		
		# Return
		return($self->{_WGSLatDec},$self->{_WGSLongDec});
	}
	else{
	
		# Give warning if nothing attached
		print "Did not fetch WGS84 data: no data has been attached!\n";
	}
}

sub fetch_WGS84_sex {

	# Read params
	my $self = shift;
	
	# Fetch decimal
	my @dec = $self->fetch_WGS84_dec();
	
	# Proceed if fetch ok
	if (scalar(@dec) > 0){
	
		# Convert to sexagesimal
		my @sex1 = DECtoSEX($dec[0]);
		my @sex2 = DECtoSEX($dec[1]);
		
		# Join array
		my @sex;
		foreach(@sex1){
			push(@sex,$_)
		}
		foreach(@sex2){
			push(@sex,$_)
		}

		# Return
		return(@sex);
	}
}

sub unattach {

	# Read params
	my $self = shift;
	
	# Empty object
	$self = {	
		_WGSLatDec   => undef,
		_WGSLongDec  => undef,
		_CH1903X     => undef,        
		_CH1903Y     => undef,	
	}
}

#############
# FUNCTIONS #
#############

###################################
## Converts WGS coordinates to   ##
## CH1903 Y coordinate           ##
##                               ##
## Input:  dd.mmss     [decimal] ##
## Output: CH1903 Y    [integer] ##
##                               ##
###################################

sub WGStoCH1903y {

	# Read params
	my @params = @_;
	my ($lat, $long) = @params;

	# Convert sexagesimal to seconds
	my @lat  = DECtoSEX($lat);
	my @long = DECtoSEX($long);
	$lat  = DEGtoSEC(@lat);
	$long = DEGtoSEC(@long);

	# Set auxiliaries
	my $lat_aux = ($lat - 169028.66)/10000;
	my $long_aux = ($long - 26782.5)/10000;

	# Process Y
	my $y = 600072.37 
	        + (211455.93 * $long_aux)
	        -  (10938.51 * $long_aux * $lat_aux)
	        -  (    0.36 * $long_aux * $lat_aux * $lat_aux)
	        -  (   44.54 * $long_aux * $long_aux*  $long_aux);

	# Return Y coordinate
	return $y;
}

###################################
## Converts WGS coordinates to   ##
## CH1903 X coordinate           ##
##                               ##
## Input:  dd.mmss     [decimal] ##
## Output: CH1903 X    [integer] ##
##                               ##
###################################

sub WGStoCH1903x {

	# Read params
	my @params = @_;
	my ($lat, $long) = @params;

	# Convert sexagesimal to seconds
	my @lat  = DECtoSEX($lat);
	my @long = DECtoSEX($long);
	$lat  = DEGtoSEC(@lat);
	$long = DEGtoSEC(@long);

	# Set auxiliaries
	my $lat_aux = ($lat - 169028.66)/10000;
	my $long_aux = ($long - 26782.5)/10000;

	# Process X
	my $x = 200147.07
	        + (308807.95 * $lat_aux) 
	        + (  3745.25 * $long_aux * $long_aux)
	        + (    76.63 * $lat_aux  * $lat_aux) 
	        - (   194.56 * $long_aux * $long_aux * $lat_aux)
	        + (   119.79 * $lat_aux  * $lat_aux  * $lat_aux);

	# Return X coordinate
	return $x; 
}

###################################
## Converts CH1903 coordinates to##
## WGS decimal latitude          ##
##                               ##
## Input:  CH1903 coor [integer] ##
## Output: dd.mmss     [decimal] ##
##                               ##
###################################

sub CH1903toWGSlat {

	# Read params
	my @params = @_;
	my ($y,$x) = @params;	

	# Set auxiliaries
	my $y_aux = ($y - 600000)/1000000;
	my $x_aux = ($x - 200000)/1000000;
  
	# Process latitude
	my $lat = 16.9023892
		+ (3.238272 * $x_aux)
		- (0.270978 * $y_aux * $y_aux)
		- (0.002528 * $x_aux * $x_aux)
		- (  0.0447 * $y_aux * $y_aux * $x_aux)
		- (  0.0140 * $x_aux * $x_aux * $x_aux);
    
	# Unit 10000" to 1 " and converts seconds to degrees
	$lat = $lat * 100/36;
  
	# Return dd.mmss
	return $lat;
}

###################################
## Converts CH1903 coordinates to##
## WGS decimal longitude         ##
##                               ##
## Input:  CH1903 coor [integer] ##
## Output: dd.mmss     [decimal] ##
##                               ##
###################################

sub CH1903toWGSlong {

	# Read params
	my @params = @_;
	my ($y,$x) = @params;	

	# Set auxiliaries
	my $y_aux = ($y - 600000)/1000000;
	my $x_aux = ($x - 200000)/1000000;
  
	# Process longitude
	my $long = 2.6779094
		+ (4.728982 * $y_aux)
		+ (0.791484 * $y_aux * $x_aux)
		+ (  0.1306 * $y_aux * $x_aux * $x_aux)
		- (  0.0436 * $y_aux * $y_aux * $y_aux);
     
	# Unit 10000" to 1 " and converts seconds to degrees
	$long = $long * 100/36;
   
	# Return dd.mmss
	return $long;
}

###################################
## Converts a sexagesimal (base 60#
## angle to a decimal angle      ##
##                               ##
## Input:  dd mm ss      [array] ##
## Output: dd.mmss     [decimal] ##
##                               ##
###################################

sub SEXtoDEC {

	# Read params
	my @params = @_;
	my ($deg,$min,$sec) = @params;	
	
	# Extract decimal
	my $dec = $deg + $min/60 + $sec/3600;

	# return dd.mmss
	return $dec;
}

###################################
## Converts a decimal angle to a ##
## sexagesimal (base 60) angle   ##
##                               ##
## Input:  dd.mmss     [decimal] ##
## Output: dd mm ss      [array] ##
##                               ##
###################################

sub DECtoSEX {

	# Read params
	my @params = @_;
	my ($angle) = @params;	

	# Extract DMS
	my $deg = int($angle);
	my $min = int(($angle-$deg)*60);
	my $sec = ((($angle-$deg)*60)-$min)*60; 

	# Clean up seconds
	$sec = int($sec + 0.5);			# Round
	if (length($sec)<2){ $sec = "0".$sec};	# Make 2 digit number

	# Return dd mm ss array
	return ($deg,$min,$sec);
}

###################################
## Converts a sexagesimal (base 60#
## angle to seconds              ##
##                               ##
## Input:  dd mm ss      [array] ##
## Output: seconds     [integer] ##
##                               ##
###################################

sub DEGtoSEC {

	# Read params
	my @params = @_;
	my ($deg,$min,$sec) = @params;	

	# Calculate seconds
	my $seconds = $sec + $min*60 + $deg*3600;
	
	# Return seconds
	return $seconds;
}

1;

__END__

=head1 NAME

Geo::Coordinates::WGS84toCH1903 - converts WGS84 coordinate system to/from the Swiss CH1903 coordinate system 

=head1 SYNOPSIS

  use Geo::Coordinates::WGS84toCH1903;

  # Constructor
  my $coordinates = new Geo::Coordinates::WGS84toCH1903;
  
  # Attach decimal WGS84 coordinates to the object
  $coordinates->attach_WGS84_dec(45.966667,7.65);
  
  # Attach sexagesimal WGS84 coordinates to the object
  $coordinates->attach_WGS84_sex(45,58,0,7,39,0);
  
  # Attach CH1903 coordinates to the object
  $coordinates->attach_CH1903(4617049,91670);

  # Return decimal WGS84 coordinates from the object (regardless of the input type)
  my ($lat,$long) = $coordinates->fetch_WGS84_dec();
  
  # Return sexagesimal WGS84 coordinates from the object (regardless of the input type)
  my @DMS_coordinates = $coordinates->fetch_WGS84_sex(); 
  
  # Return CH1903 coordinates from the object (regardless of the input type)
  my ($x,$y) = $coordinates->fetch_CH1903();
  
  # Clear the object
  $coordinates->unattach();
  
=head1 DESCRIPTION

This module converts WGS84 coordinate system to/from the Swiss CH1903 coordinate system. The WGS84 coordinate 
system is what many people know from maps and considered 'the default'. The CH1903 coordinate system covers 
Switzerland and surroundings. The coordinate system and the transformation functions are defined by the 
Official Federal Office of Topography of Switzerland (swisstopo).

The 0 / 0 coordinate is located near Bordeaux, France. This definition invokes that CH1903 coordinates are only
usefull for places, such as Switzerland, located in the 1st quadrant (i.e. NE of this point) of the coordinate
system.

=head1 METHODS

=over 8

=item attach_WGS84_dec(latitude, longitude)

    Attaches a decimal WGS84 coordinate to the object.
    
    Needs a two-dimensional array as input where both latitude and longitude are decimal. 
    As the coordinate system is designed to be used in Switzerland, input only N and E locations.
    
    This method has no return values.
    
=item attach_WGS84_sex(degrees, minutes, seconds, degrees, minutes, seconds)

    Attaches a sexagesimal (base 60) WGS84 coordinate to the object.
    
    Needs a six-dimensional array as input where both latitude and longitude are described by degree, minutes 
    and seconds. E.g. 45°58'35''N / 7°39'31''E would be (45,58,35,7,39,31) as input.
    As the coordinate system is designed to be used in Switzerland, input only N and E locations
    
    This method has no return values.

=item attach_CH1903(y, x)

    Attaches a CH1903 coordinate to the object.
    
    Needs a two-dimensional array input where both coordinates are integers.
    
    This method has no return values.

=item fetch_WGS84_dec()

    This method has no input values.

    Returns a decimal WGS84 coordinate if a decimal/sexagesimal WGS84 coordinate was attached to the object.
    If a CH1903 coordinate was attached to the object, the coordinate is converted to WGS84, stored in the object 
    , and returned.
    
    The return value is a two-dimensional array with latitude and longitude.

=item fetch_WGS84_sex()

    This method has no input values.

    Returns a sexagesimal WGS84 coordinate if a decimal/sexagesimal WGS84 coordinate was attached to the object.
    If a CH1903 coordinate was attached to the object, the coordinate is converted to WGS84, stored in the object 
    , and returned.
    
    The return value is a six-dimensional array with both the latitude and longitude in sexagesimal DMS coordinates.

=item fetch_CH1903()

   This method has no input values.
   
   Return a CH1903 coordinate if a CH1903 coordinate was attached to the object.
   If a decimal/sexagesimal WGS84 coordinate was attached to the object, the coordinate is converted to CH1903,
   stored in the object, and returned.
   
   The return value is a two-dimensional array with both the y and x CH1903 coordinate.


=item unattach()

   This method has no input and no return values.
   
   Empties the object.

=back

=head1 BUGS

Please report to author.

=head1 SEE ALSO

Official Federal Office of Topography swisstopo website
http://www.swisstopo.admin.ch/internet/swisstopo/en/home/products/software/products/skripts.html

Wikipedia page on the CH1903 coordinates
http://en.wikipedia.org/wiki/Swiss_coordinate_system

Online conversion tool for WGS84<>CH1903 coordinats
http://www.swisstopo.admin.ch/internet/swisstopo/en/home/apps/calc/navref.html

=head1 AUTHOR

CPAN package: Joachim De Schrijver, <joachim.deschrijver@gmail.com>

=head1 ACKNOWLEDGEMENTS

The Perl realization is based on the implementations provided by Federal Office of Topography swisstopo. 

The implementations are available on the abovementioned swisstopo website:

=head1 LICENSE AND COPYRIGHT

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.10.1 or,
at your option, any later version of Perl 5 you may have available.

Copyright (C) 2012 Joachim De Schrijver

=cut