<?php

/**
 * PHP class to convert Latitude+Longitude coordinates into UTM and wise versa.
 * 
 * Code for datum and UTM conversion was converted from C++ code written by Chuck Gantz (chuck.gantz@globalstar.com) from http://www.gpsy.com/gpsinfo/geotoutm/
 * The C++ code was refactored and rewritten into PHP code by Hans Duedal (hd@onlinecity.dk).
 * The PHP conversion was inspired by work done by Brenor Brophy (brenor@sbcglobal.net), but derived from the "original" C++ source.
 * 
 * @author chuck.gantz@globalstar.com, hd@onlinecity.dk
 * 
 * GpointConverter (conversion between geographic points) Copyright (C) 2011 Hans Duedal (hd@onlinecity.dk)
 * 
 * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as 
 * published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of 
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * This license can be read at: http://www.opensource.org/licenses/lgpl-2.1.php
 * 
 * @link http://www.gpsy.com/gpsinfo/geotoutm/
 * @link https://gist.github.com/840476
 */
class GpointConverter
{
	const K0 = 0.9996;
	
	/**
	 * Equatorial Radius
	 * @var integer
	 */
	private $a;
	
	/**
	 * Square of eccentricity
	 * @var float
	 */
	private $eccSquared;
	
	public function __construct($datumName='ETRS89')
	{
		$this->setEllipsoid($datumName);
		$this->datum = $datumName;
	}

	/**
	 * Convert latitude/longitude into UTM coordinates. Equations from USGS Bulletin 1532 
	 * Automatically calculates the zone, with special zone rules added for Denmark and Svalbard.
	 * Denmark stretches the zone 32 in ETRS89 to include all of Zealand so users don't have to deal with zone crossings.
	 * 
	 * @param float $latitude
	 * @param float $longitude
	 */
	public function convertLatLngToUtm($latitude, $longitude)
	{
		//Make sure the longitude is between -180.00 .. 179.9
		$LongTemp = ($longitude+180)-(int) (($longitude+180)/360)*360-180; // -180.00 .. 179.9;
		$LatRad = deg2rad($latitude);
		$LongRad = deg2rad($LongTemp);
		
		if ($LongTemp >= 8 && $LongTemp <= 13 && $latitude > 54.5 && $latitude < 58) { // Special zones for Denmark: http://www.kms.dk/Referencenet/Referencesystemer/UTM_ETRS89/
			$ZoneNumber = 32;
		} else if( $latitude >= 56.0 && $latitude < 64.0 && $LongTemp >= 3.0 && $LongTemp < 12.0 ) { // From C++ code
			$ZoneNumber = 32;
		} else {
			$ZoneNumber = (int) (($LongTemp + 180)/6) + 1;
			// Special zones for Svalbard
			if( $latitude >= 72.0 && $latitude < 84.0 ) {
				if($LongTemp >= 0.0 && $LongTemp < 9.0) {
					$ZoneNumber = 31;
				} else if($LongTemp >= 9.0 && $LongTemp < 21.0) {
					$ZoneNumber = 33;
				} else if($LongTemp >= 21.0 && $LongTemp < 33.0) {
					$ZoneNumber = 35;
				} else if($LongTemp >= 33.0 && $LongTemp < 42.0) {
					$ZoneNumber = 37;
				}
			}
		}
		
		$LongOrigin = ($ZoneNumber - 1)*6 - 180 + 3;  //+3 puts origin in middle of zone
		$LongOriginRad = deg2rad($LongOrigin);
		
		$UTMZone = $ZoneNumber.self::getUtmLetterDesignator($latitude);
		
		$eccPrimeSquared = ($this->eccSquared)/(1-$this->eccSquared);
		
		$N = $this->a/sqrt(1-$this->eccSquared*sin($LatRad)*sin($LatRad));
		$T = tan($LatRad)*tan($LatRad);
		$C = $eccPrimeSquared*cos($LatRad)*cos($LatRad);
		$A = cos($LatRad)*($LongRad-$LongOriginRad);
	
		$M = $this->a*((1	- $this->eccSquared/4		- 3*$this->eccSquared*$this->eccSquared/64	- 5*$this->eccSquared*$this->eccSquared*$this->eccSquared/256)*$LatRad 
					- (3*$this->eccSquared/8	+ 3*$this->eccSquared*$this->eccSquared/32	+ 45*$this->eccSquared*$this->eccSquared*$this->eccSquared/1024)*sin(2*$LatRad)
										+ (15*$this->eccSquared*$this->eccSquared/256 + 45*$this->eccSquared*$this->eccSquared*$this->eccSquared/1024)*sin(4*$LatRad) 
										- (35*$this->eccSquared*$this->eccSquared*$this->eccSquared/3072)*sin(6*$LatRad));
		
		$UTMEasting = (float)(self::K0*$N*($A+(1-$T+$C)*$A*$A*$A/6
						+ (5-18*$T+$T*$T+72*$C-58*$eccPrimeSquared)*$A*$A*$A*$A*$A/120)
						+ 500000.0);
	
		$UTMNorthing = (float)(self::K0*($M+$N*tan($LatRad)*($A*$A/2+(5-$T+9*$C+4*$C*$C)*$A*$A*$A*$A/24
					 + (61-58*$T+$T*$T+600*$C-330*$eccPrimeSquared)*$A*$A*$A*$A*$A*$A/720)));
		if($latitude < 0)	$UTMNorthing += 10000000.0; //10000000 meter offset for southern hemisphere
		
		// Round them off, it's normally specified as integers and conversion is not terribly exact anyway
		$UTMNorthing = (int) round($UTMNorthing);
		$UTMEasting = (int) round($UTMEasting);

		return array($UTMEasting,$UTMNorthing,$UTMZone);
	}
	
	/**
	 * Convert UTM to Longitude/Latitude
	 * 
	 * Equations from USGS Bulletin 1532.
	 * East Longitudes are positive, West longitudes are negative.
	 * North latitudes are positive, South latitudes are negative
	 * Lat and Long are in decimal degrees. 
	 * 
	 * @param integer $UTMEasting
	 * @param integer $UTMNorthing
	 * @param string $UTMZone
	 */
	public function convertUtmToLatLng($UTMEasting, $UTMNorthing, $UTMZone)
	{
		$e1 = (1-sqrt(1-$this->eccSquared))/(1+sqrt(1-$this->eccSquared));
		$x = $UTMEasting - 500000.0; //remove 500,000 meter offset for longitude
		$y = $UTMNorthing;
	
		sscanf($UTMZone,"%d%s",$ZoneNumber,$ZoneLetter);
		
		if (strcmp('N',$ZoneLetter) <= 0) {
			$NorthernHemisphere = 1;//point is in northern hemisphere
		} else {
			$NorthernHemisphere = 0;//point is in southern hemisphere
			$y -= 10000000.0;//remove 10,000,000 meter offset used for southern hemisphere
		}
	
		$LongOrigin = ($ZoneNumber - 1)*6 - 180 + 3;  //+3 puts origin in middle of zone
	
		$eccPrimeSquared = ($this->eccSquared)/(1-$this->eccSquared);
	
		$M = $y / self::K0;
		$mu = $M/($this->a*(1-$this->eccSquared/4-3*$this->eccSquared*$this->eccSquared/64-5*$this->eccSquared*$this->eccSquared*$this->eccSquared/256));
	
		$phi1Rad = $mu	+ (3*$e1/2-27*$e1*$e1*$e1/32)*sin(2*$mu) 
					+ (21*$e1*$e1/16-55*$e1*$e1*$e1*$e1/32)*sin(4*$mu)
					+(151*$e1*$e1*$e1/96)*sin(6*$mu);
		$phi1 = rad2deg($phi1Rad);
	
		$N1 = $this->a/sqrt(1-$this->eccSquared*sin($phi1Rad)*sin($phi1Rad));
		$T1 = tan($phi1Rad)*tan($phi1Rad);
		$C1 = $eccPrimeSquared*cos($phi1Rad)*cos($phi1Rad);
		$R1 = $this->a*(1-$this->eccSquared)/pow(1-$this->eccSquared*sin($phi1Rad)*sin($phi1Rad), 1.5);
		$D = $x/($N1*self::K0);
	
		$Lat = $phi1Rad - ($N1*tan($phi1Rad)/$R1)*($D*$D/2-(5+3*$T1+10*$C1-4*$C1*$C1-9*$eccPrimeSquared)*$D*$D*$D*$D/24
						+(61+90*$T1+298*$C1+45*$T1*$T1-252*$eccPrimeSquared-3*$C1*$C1)*$D*$D*$D*$D*$D*$D/720);
		$Lat = rad2deg($Lat);
	
		$Long = ($D-(1+2*$T1+$C1)*$D*$D*$D/6+(5-2*$C1+28*$T1-3*$C1*$C1+8*$eccPrimeSquared+24*$T1*$T1)
						*$D*$D*$D*$D*$D/120)/cos($phi1Rad);
		$Long = $LongOrigin + rad2deg($Long);
		return array($Lat,$Long);
	}

	/**
	 * Reference ellipsoids derived from Peter H. Dana's website: 
	 * 	http://www.utexas.edu/depts/grg/gcraft/notes/datum/elist.html
	 * 	Department of Geography, University of Texas at Austin
	 * 	Internet: pdana@mail.utexas.edu 3/22/95
	 * Source:
	 * 	Defense Mapping Agency. 1987b. DMA Technical Report: Supplement to Department of Defense World Geodetic System 1984 Technical Report. Part I and II.
	 * 	Washington, DC: Defense Mapping Agency
	 * Alternative names added in for easy compatibility by hd@onlinecity.dk
	 * 
	 * @param string $name
	 */
	public function setEllipsoid($name)
	{
		switch ($name) {
			case 'Airy': $this->a = 6377563;$this->eccSquared = 0.00667054;break;
			case 'Australian National': $this->a = 6378160;$this->eccSquared = 0.006694542;break;
			case 'Bessel 1841': $this->a = 6377397;$this->eccSquared = 0.006674372;break;
			case 'Bessel 1841 Nambia': $this->a = 6377484;$this->eccSquared = 0.006674372;break;
			case 'Clarke 1866': $this->a = 6378206;$this->eccSquared = 0.006768658;break;
			case 'Clarke 1880': $this->a = 6378249;$this->eccSquared = 0.006803511;break;
			case 'Everest': $this->a = 6377276;$this->eccSquared = 0.006637847;break;
			case 'Fischer 1960 Mercury': $this->a = 6378166;$this->eccSquared = 0.006693422;break;
			case 'Fischer 1968': $this->a = 6378150;$this->eccSquared = 0.006693422;break;
			case 'GRS 1967': $this->a = 6378160;$this->eccSquared = 0.006694605;break;
			case 'GRS 1980': $this->a = 6378137;$this->eccSquared = 0.00669438;break;
			case 'Helmert 1906': $this->a = 6378200;$this->eccSquared = 0.006693422;break;
			case 'Hough': $this->a = 6378270;$this->eccSquared = 0.00672267;break;
			case 'International': $this->a = 6378388;$this->eccSquared = 0.00672267;break;
			case 'Krassovsky': $this->a = 6378245;$this->eccSquared = 0.006693422;break;
			case 'Modified Airy': $this->a = 6377340;$this->eccSquared = 0.00667054;break;
			case 'Modified Everest': $this->a = 6377304;$this->eccSquared = 0.006637847;break;
			case 'Modified Fischer 1960': $this->a = 6378155;$this->eccSquared = 0.006693422;break;
			case 'South American 1969': $this->a = 6378160;$this->eccSquared = 0.006694542;break;
			case 'WGS 60': $this->a = 6378165;$this->eccSquared = 0.006693422;break;
			case 'WGS 66': $this->a = 6378145;$this->eccSquared = 0.006694542;break;
			case 'WGS 72': $this->a = 6378135;$this->eccSquared = 0.006694318;break;
			case 'ED50': $this->a = 6378388;$this->eccSquared = 0.00672267;break; // International Ellipsoid
			case 'WGS 84':
			case 'EUREF89': // Max deviation from WGS 84 is 40 cm/km see http://ocq.dk/euref89 (in danish)
			case 'ETRS89': // Same as EUREF89 
				$this->a = 6378137;
				$this->eccSquared = 0.00669438;
				break;
			default:
				throw new \InvalidArgumentException('No ecclipsoid data associated with unknown datum: '.$name);
		}
	}
	
	/**
	 * Get the UTM letter designator for a given latitude.
	 * returns 'Z' if latitude is outside the UTM limits of 84N to 80S
	 * 
	 * @param float $latitude
	 */
	public static function getUtmLetterDesignator($latitude)
	{
		switch ($latitude) {
			case ((84 >= $latitude) && ($latitude >= 72)): return 'X';
			case ((72 > $latitude) && ($latitude >= 64)): return 'W';
			case ((64 > $latitude) && ($latitude >= 56)): return 'V';
			case ((56 > $latitude) && ($latitude >= 48)): return 'U';
			case ((48 > $latitude) && ($latitude >= 40)): return 'T';
			case ((40 > $latitude) && ($latitude >= 32)): return 'S';
			case ((32 > $latitude) && ($latitude >= 24)): return 'R';
			case ((24 > $latitude) && ($latitude >= 16)): return 'Q';
			case ((16 > $latitude) && ($latitude >= 8)): return 'P';
			case (( 8 > $latitude) && ($latitude >= 0)): return 'N';
			case (( 0 > $latitude) && ($latitude >= -8)): return 'M';
			case ((-8 > $latitude) && ($latitude >= -16)): return 'L';
			case ((-16 > $latitude) && ($latitude >= -24)): return 'K';
			case ((-24 > $latitude) && ($latitude >= -32)): return 'J';
			case ((-32 > $latitude) && ($latitude >= -40)): return 'H';
			case ((-40 > $latitude) && ($latitude >= -48)): return 'G';
			case ((-48 > $latitude) && ($latitude >= -56)): return 'F';
			case ((-56 > $latitude) && ($latitude >= -64)): return 'E';
			case ((-64 > $latitude) && ($latitude >= -72)): return 'D';
			case ((-72 > $latitude) && ($latitude >= -80)): return 'C';
			default: return 'Z';			
		}
	}
}