toSixFigureString() so that the eastings and northings // are rounded rather than floored. // 2.2 - 11 Feb 2006 // - Used different algorithm for calculating distance between latitudes // and longitudes - fixes a number of problems with distance calculations // 2.1 - 22 Dec 2005 // - Added getOSRefFromSixFigureReference function // 2.0 - 21 Dec 2005 // - Completely different object design - conversion functions now through // objects rather than static functions // - Updated comments and documentation // 1.1 - 11 Sep 2005 // - Added OSGB36/WGS84 data conversions // 1.0 - 11 Aug 2005 // - Initial version //-------------------------------------------------------------------------- // ================================================================== LatLng class LatLng { public $lat; public $lng; /** * Create a new LatLng object from the given latitude and longitude * * @param float|integer lat latitude * @param float|integer lng longitude */ public function __construct($lat, $lng) { $this->lat = $lat; $this->lng = $lng; } /** * Return a string representation of this LatLng object * * @return string representation of this LatLng object */ public function toString() { return "({$this->lat}, {$this->lng})"; } /** * Calculate the surface distance between this LatLng object and the one * passed in as a parameter. * * @param array $to a LatLng object to measure the surface distance to * @return float|integer the surface distance */ public function distance($to) { $er = 6366.707; $latFrom = deg2rad($this->lat); $latTo = deg2rad($to['lat']); $lngFrom = deg2rad($this->lng); $lngTo = deg2rad($to['lng']); $x1 = $er * cos($lngFrom) * sin($latFrom); $y1 = $er * sin($lngFrom) * sin($latFrom); $z1 = $er * cos($latFrom); $x2 = $er * cos($lngTo) * sin($latTo); $y2 = $er * sin($lngTo) * sin($latTo); $z2 = $er * cos($latTo); $d = acos(sin($latFrom)*sin($latTo) + cos($latFrom)*cos($latTo)*cos($lngTo-$lngFrom)) * $er; return $d; } /** * Convert this LatLng object from OSGB36 datum to WGS84 datum. */ public function OSGB36ToWGS84() { $airy1830 = new RefEll(6377563.396, 6356256.909); $a = $airy1830->maj; $b = $airy1830->min; $eSquared = $airy1830->ecc; $phi = deg2rad($this->lat); $lambda = deg2rad($this->lng); $v = $a / (sqrt(1 - $eSquared * sinSquared($phi))); $H = 0; // height $x = ($v + $H) * cos($phi) * cos($lambda); $y = ($v + $H) * cos($phi) * sin($lambda); $z = ((1 - $eSquared) * $v + $H) * sin($phi); $tx = 446.448; $ty = -124.157; $tz = 542.060; $s = -0.0000204894; $rx = deg2rad( 0.00004172222); $ry = deg2rad( 0.00006861111); $rz = deg2rad( 0.00023391666); $xB = $tx + $x * ( 1 + $s ) + -$rx * $y + $ry * $z; $yB = $ty + $rz * $x + $y * ( 1 + $s ) + -$rx * $z; $zB = $tz + -$ry * $x + $rx * $y + $z * ( 1 + $s ); $wgs84 = new RefEll(6378137.000, 6356752.3141); $a = $wgs84->maj; $b = $wgs84->min; $eSquared = $wgs84->ecc; $lambdaB = rad2deg(atan($yB / $xB)); $p = sqrt( $xB * $xB + $yB * $yB ); $phiN = atan($zB / ($p * (1 - $eSquared))); for ($i = 1; $i < 10; $i++) { $v = $a / (sqrt(1 - $eSquared * sinSquared($phiN))); $phiN1 = atan(($zB + $eSquared * $v * sin( $phiN )) / $p); $phiN = $phiN1; } $phiB = rad2deg($phiN); $this->lat = $phiB; $this->lng = $lambdaB; } /** * Convert this LatLng object from WGS84 datum to OSGB36 datum. */ function WGS84ToOSGB36() { $wgs84 = new RefEll(6378137.000, 6356752.3141); $a = $wgs84->maj; $b = $wgs84->min; $eSquared = $wgs84->ecc; $phi = deg2rad($this->lat); $lambda = deg2rad($this->lng); $v = $a / (sqrt(1 - $eSquared * sinSquared($phi))); $H = 0; // height $x = ($v + $H) * cos($phi) * cos($lambda); $y = ($v + $H) * cos($phi) * sin($lambda); $z = ((1 - $eSquared) * $v + $H) * sin($phi); $tx = -446.448; $ty = 124.157; $tz = -542.060; $s = 0.0000204894; $rx = deg2rad(-0.00004172222); $ry = deg2rad(-0.00006861111); $rz = deg2rad(-0.00023391666); $xB = $tx + $x * ( 1 + $s ) + -$rx * $y + $ry * $z; $yB = $ty + $rz * $x + $y * ( 1 + $s ) + -$rx * $z; $zB = $tz + -$ry * $x + $rx * $y + $z * ( 1 + $s ); $airy1830 = new RefEll(6377563.396, 6356256.909); $a = $airy1830->maj; $b = $airy1830->min; $eSquared = $airy1830->ecc; $lambdaB = rad2deg(atan($yB / $xB)); $p = sqrt( $xB * $xB + $yB * $yB ); $phiN = atan($zB / ($p * (1 - $eSquared))); for ($i = 1; $i < 10; $i++) { $v = $a / (sqrt(1 - $eSquared * sinSquared($phiN))); $phiN1 = atan(($zB + $eSquared * $v * sin( $phiN )) / $p); $phiN = $phiN1; } $phiB = rad2deg($phiN); $this->lat = $phiB; $this->lng = $lambdaB; } /** * Convert this LatLng object into an OSGB grid reference. Note that this * function does not take into account the bounds of the OSGB grid - * beyond the bounds of the OSGB grid, the resulting OSRef object has no * meaning * * @return string the converted OSGB grid reference */ public function toOSRef() { $airy1830 = new RefEll(6377563.396, 6356256.909); $OSGB_F0 = 0.9996012717; $N0 = -100000.0; $E0 = 400000.0; $phi0 = deg2rad(49.0); $lambda0 = deg2rad(-2.0); $a = $airy1830->maj; $b = $airy1830->min; $eSquared = $airy1830->ecc; $phi = deg2rad($this->lat); $lambda = deg2rad($this->lng); $E = 0.0; $N = 0.0; $n = ($a - $b) / ($a + $b); $v = $a * $OSGB_F0 * pow(1.0 - $eSquared * sinSquared($phi), -0.5); $rho = $a * $OSGB_F0 * (1.0 - $eSquared) * pow(1.0 - $eSquared * sinSquared($phi), -1.5); $etaSquared = $v / $rho - 1.0; $M = ($b * $OSGB_F0) * ( ( 1 + $n + ( 5.0 / 4.0 ) * $n * $n + ( 5.0 / 4.0 ) * $n * $n * $n ) * ( $phi - $phi0 ) - ( 3 * $n + 3 * $n * $n + ( 21.0 / 8.0 ) * $n * $n * $n ) * sin( $phi - $phi0 ) * cos( $phi + $phi0 ) + ( ( 15.0 / 8.0 ) * $n * $n + ( 15.0 / 8.0 ) * $n * $n * $n ) * sin( 2.0 * ( $phi - $phi0 ) ) * cos( 2.0 * ( $phi + $phi0 ) ) - ( ( 35.0 / 24.0 ) * $n * $n * $n ) * sin( 3.0 * ( $phi - $phi0 ) ) * cos( 3.0 * ( $phi + $phi0 ) )); $I = $M + $N0; $II = ($v / 2.0) * sin($phi) * cos($phi); $III = ($v / 24.0) * sin($phi) * pow(cos($phi), 3.0) * (5.0 - tanSquared($phi) + 9.0 * $etaSquared); $IIIA = ($v / 720.0) * sin($phi) * pow(cos($phi), 5.0) * (61.0 - 58.0 * tanSquared( $phi ) + pow(tan($phi), 4.0)); $IV = $v * cos($phi); $V = ($v / 6.0) * pow(cos($phi), 3.0) * ( $v / $rho - tanSquared($phi)); $VI = ($v / 120.0) * pow(cos($phi), 5.0) * (5.0 - 18.0 * tanSquared( $phi ) + (pow(tan($phi), 4.0)) + 14 * $etaSquared - 58 * tanSquared( $phi ) * $etaSquared); $N = $I + $II * pow( $lambda - $lambda0, 2.0 ) + $III * pow( $lambda - $lambda0, 4.0 ) + $IIIA * pow( $lambda - $lambda0, 6.0 ); $E = $E0 + $IV * ( $lambda - $lambda0 ) + $V * pow( $lambda - $lambda0, 3.0 ) + $VI * pow( $lambda - $lambda0, 5.0 ); $osref = new OSRef($E, $N); return $osref->toString(); } /** * Convert a latitude and longitude to an UTM reference * * @return string the converted UTM reference */ public function toUTMRef() { $wgs84 = new RefEll(6378137, 6356752.314); $UTM_F0 = 0.9996; $a = $wgs84->maj; $eSquared = $wgs84->ecc; $longitude = $this->lng; $latitude = $this->lat; $latitudeRad = $latitude * (pi() / 180.0); $longitudeRad = $longitude * (pi() / 180.0); $longitudeZone = (int) (($longitude + 180.0) / 6.0) + 1; // Special zone for Norway if ($latitude >= 56.0 && $latitude < 64.0 && $longitude >= 3.0 && $longitude < 12.0) { $longitudeZone = 32; } // Special zones for Svalbard if ($latitude >= 72.0 && $latitude < 84.0) { if ($longitude >= 0.0 && $longitude < 9.0) { $longitudeZone = 31; } else if ($longitude >= 9.0 && $longitude < 21.0) { $longitudeZone = 33; } else if ($longitude >= 21.0 && $longitude < 33.0) { $longitudeZone = 35; } else if ($longitude >= 33.0 && $longitude < 42.0) { $longitudeZone = 37; } } $longitudeOrigin = ($longitudeZone - 1) * 6 - 180 + 3; $longitudeOriginRad = $longitudeOrigin * (pi() / 180.0); $UTMZone = getUTMLatitudeZoneLetter($latitude); $ePrimeSquared = $eSquared / (1 - $eSquared); $n = $a / sqrt(1 - $eSquared * sin($latitudeRad) * sin($latitudeRad)); $t = tan($latitudeRad) * tan($latitudeRad); $c = $ePrimeSquared * cos($latitudeRad) * cos($latitudeRad); $A = cos($latitudeRad) * ($longitudeRad - $longitudeOriginRad); $M = $a * ((1 - $eSquared / 4 - 3 * $eSquared * $eSquared / 64 - 5 * $eSquared * $eSquared * $eSquared / 256) * $latitudeRad - (3 * $eSquared / 8 + 3 * $eSquared * $eSquared / 32 + 45 * $eSquared * $eSquared * $eSquared / 1024) * sin(2 * $latitudeRad) + (15 * $eSquared * $eSquared / 256 + 45 * $eSquared * $eSquared * $eSquared / 1024) * sin(4 * $latitudeRad) - (35 * $eSquared * $eSquared * $eSquared / 3072) * sin(6 * $latitudeRad)); $UTMEasting = (float) ($UTM_F0 * $n * ($A + (1 - $t + $c) * pow($A, 3.0) / 6 + (5 - 18 * $t + $t * $t + 72 * $c - 58 * $ePrimeSquared) * pow($A, 5.0) / 120) + 500000.0); $UTMNorthing = (float) ($UTM_F0 * ($M + $n * tan($latitudeRad) * ($A * $A / 2 + (5 - $t + 9 * $c + 4 * $c * $c ) * pow($A, 4.0) / 24 + (61 - 58 * $t + $t * $t + 600 * $c - 330 * $ePrimeSquared ) * pow($A, 6.0) / 720))); // Adjust for the southern hemisphere if ($latitude < 0) { $UTMNorthing += 10000000.0; } $utmref = new UTMRef($UTMEasting, $UTMNorthing, $UTMZone, $longitudeZone); return $utmref->toString(); } } // =================================================================== OSRef // References given with OSRef are accurate to 1m. class OSRef { public $easting; public $northing; /** * Create a new OSRef object representing an OSGB grid reference. Note * that the parameters for this constructor require eastings and * northings with 1m accuracy and need to be absolute with respect to * the whole of the British Grid. For example, to create an OSRef * object from the six-figure grid reference TG514131, the easting would * be 651400 and the northing would be 313100. * * Grid references with accuracy greater than 1m can be represented * using floating point values for the easting and northing. For example, * a value representing an easting or northing accurate to 1mm would be * given as 651400.0001. * * @param float|integer easting the easting of the reference (with 1m accuracy) * @param float|integer northing the northing of the reference (with 1m accuracy) */ public function __construct($easting, $northing) { $this->easting = $easting; $this->northing = $northing; } /** * Convert this grid reference into a string showing the exact values * of the easting and northing. * * @return */ public function toString() { return "({$this->easting}, {$this->northing})"; } /** * Convert this grid reference into a string using a standard six-figure * grid reference including the two-character designation for the 100km * square. e.g. TG514131. * * @return */ public function toSixFigureString() { $hundredkmE = floor($this->easting / 100000); $hundredkmN = floor($this->northing / 100000); $firstLetter = ""; $firstLetter = $hundredkmN < 5 ? ( $hundredkmE < 5 ? "S" : "T" ) : ( $hundredkmN < 10 ? ( $hundredkmE < 5 ? "N" : "O" ) : "H" ); $secondLetter = ""; $index = 65 + ( 4 - ( $hundredkmN % 5 ) ) * 5 + ($hundredkmE % 5); $ti = $index; if ($index >= 73) $index++; $secondLetter = chr($index); $e = round(($this->easting - 100000 * $hundredkmE) / 100); $n = round(($this->northing - 100000 * $hundredkmN) / 100); return sprintf("%s%s%03d%03d", $firstLetter, $secondLetter, $e, $n); } /** * Convert this grid reference into a latitude and longitude * * @return */ public function toLatLng() { $airy1830 = new RefEll(6377563.396, 6356256.909); $OSGB_F0 = 0.9996012717; $N0 = -100050.0; $E0 = 400110.0; $phi0 = deg2rad(49.0); $lambda0 = deg2rad(-2.0); $a = $airy1830->maj; $b = $airy1830->min; $eSquared = $airy1830->ecc; $phi = 0.0; $lambda = 0.0; $E = $this->easting; $N = $this->northing; $n = ($a - $b) / ($a + $b); $M = 0.0; $phiPrime = ( $N - $N0 ) / ( $a * $OSGB_F0 ) + $phi0; do { $M = ($b * $OSGB_F0) * ( ( 1 + $n + ( 5.0 / 4.0 ) * $n * $n + ( 5.0 / 4.0 ) * $n * $n * $n ) * ( $phiPrime - $phi0 ) - ( 3 * $n + 3 * $n * $n + ( 21.0 / 8.0 ) * $n * $n * $n ) * sin( $phiPrime - $phi0 ) * cos( $phiPrime + $phi0 ) + ( ( 15.0 / 8.0 ) * $n * $n + ( 15.0 / 8.0 ) * $n * $n * $n ) * sin( 2.0 * ( $phiPrime - $phi0 ) ) * cos( 2.0 * ( $phiPrime + $phi0 ) ) - ( ( 35.0 / 24.0 ) * $n * $n * $n ) * sin( 3.0 * ( $phiPrime - $phi0 ) ) * cos( 3.0 * ( $phiPrime + $phi0 ) )); $phiPrime += ($N - $N0 - $M) / ($a * $OSGB_F0); } while (($N - $N0 - $M) >= 0.001); $v = $a * $OSGB_F0 * pow(1.0 - $eSquared * sinSquared($phiPrime), -0.5); $rho = $a * $OSGB_F0 * (1.0 - $eSquared) * pow(1.0 - $eSquared * sinSquared($phiPrime), -1.5); $etaSquared = $v / $rho - 1.0; $VII = tan($phiPrime) / (2 * $rho * $v); $VIII = (tan($phiPrime) / (24.0 * $rho * pow($v, 3.0))) * (5.0 + 3.0 * tanSquared( $phiPrime ) + $etaSquared - 9.0 * tanSquared( $phiPrime ) * $etaSquared); $IX = (tan($phiPrime) / (720.0 * $rho * pow($v, 5.0))) * (61.0 + 90.0 * tanSquared( $phiPrime ) + 45.0 * tanSquared( $phiPrime ) * tanSquared( $phiPrime )); $X = sec($phiPrime) / $v; $XI = (sec($phiPrime) / (6.0 * $v * $v * $v)) * ( $v / $rho + 2 * tanSquared( $phiPrime ) ); $XII = (sec($phiPrime) / (120.0 * pow($v, 5.0))) * (5.0 + 28.0 * tanSquared( $phiPrime ) + 24.0 * tanSquared( $phiPrime ) * tanSquared( $phiPrime )); $XIIA = (sec($phiPrime) / (5040.0 * pow($v, 7.0))) * (61.0 + 662.0 * tanSquared( $phiPrime ) + 1320.0 * tanSquared( $phiPrime ) * tanSquared( $phiPrime ) + 720.0 * tanSquared( $phiPrime ) * tanSquared( $phiPrime ) * tanSquared( $phiPrime )); $phi = $phiPrime - $VII * pow( $E - $E0, 2.0 ) + $VIII * pow( $E - $E0, 4.0 ) - $IX * pow( $E - $E0, 6.0 ); $lambda = $lambda0 + $X * ( $E - $E0 ) - $XI * pow( $E - $E0, 3.0 ) + $XII * pow( $E - $E0, 5.0 ) - $XIIA * pow( $E - $E0, 7.0 ); return new LatLng(rad2deg($phi), rad2deg($lambda)); } } // ================================================================== UTMRef class UTMRef { public $easting; public $northing; public $latZone; public $lngZone; /** * Create a new object representing a UTM reference. * * @param float|integer easting * @param float|integer northing * @param float|integer latZone * @param float|integer lngZone */ public function __construct($easting, $northing, $latZone, $lngZone) { $this->easting = $easting; $this->northing = $northing; $this->latZone = $latZone; $this->lngZone = $lngZone; } /** * Return a string representation of this UTM reference * * @return string */ public function toString() { return "{$this->lngZone}{$this->latZone} {$this->easting} {$this->northing}"; } /** * Convert this UTM reference to a latitude and longitude * * @return string the converted latitude and longitude */ public function toLatLng() { $wgs84 = new RefEll(6378137, 6356752.314); $UTM_F0 = 0.9996; $a = $wgs84->maj; $eSquared = $wgs84->ecc; $ePrimeSquared = $eSquared / (1.0 - $eSquared); $e1 = (1 - sqrt(1 - $eSquared)) / (1 + sqrt(1 - $eSquared)); $x = $this->easting - 500000.0;; $y = $this->northing; $zoneNumber = $this->lngZone; $zoneLetter = $this->latZone; $longitudeOrigin = ($zoneNumber - 1.0) * 6.0 - 180.0 + 3.0; // Correct y for southern hemisphere if ((ord($zoneLetter) - ord("N")) < 0) { $y -= 10000000.0; } $m = $y / $UTM_F0; $mu = $m / ($a * (1.0 - $eSquared / 4.0 - 3.0 * $eSquared * $eSquared / 64.0 - 5.0 * pow($eSquared, 3.0) / 256.0)); $phi1Rad = $mu + (3.0 * $e1 / 2.0 - 27.0 * pow($e1, 3.0) / 32.0) * sin(2.0 * $mu) + (21.0 * $e1 * $e1 / 16.0 - 55.0 * pow($e1, 4.0) / 32.0) * sin(4.0 * $mu) + (151.0 * pow($e1, 3.0) / 96.0) * sin(6.0 * $mu); $n = $a / sqrt(1.0 - $eSquared * sin($phi1Rad) * sin($phi1Rad)); $t = tan($phi1Rad) * tan($phi1Rad); $c = $ePrimeSquared * cos($phi1Rad) * cos($phi1Rad); $r = $a * (1.0 - $eSquared) / pow( 1.0 - $eSquared * sin($phi1Rad) * sin($phi1Rad), 1.5, ); $d = $x / ($n * $UTM_F0); $latitude = ( $phi1Rad - ($n * tan($phi1Rad) / $r) * ($d * $d / 2.0 - (5.0 + 3.0 * $t + 10.0 * $c - 4.0 * $c * $c - 9.0 * $ePrimeSquared) * pow($d, 4.0) / 24.0 + (61.0 + 90.0 * $t + 298.0 * $c + 45.0 * $t * $t - 252.0 * $ePrimeSquared - 3.0 * $c * $c) * pow($d, 6.0) / 720.0)) * (180.0 / pi()); $longitude = $longitudeOrigin + ( ($d - (1.0 + 2.0 * $t + $c) * pow($d, 3.0) / 6.0 + (5.0 - 2.0 * $c + 28.0 * $t - 3.0 * $c * $c + 8.0 * $ePrimeSquared + 24.0 * $t * $t) * pow($d, 5.0) / 120.0) / cos($phi1Rad)) * (180.0 / pi()); $latlog = new LatLng($latitude, $longitude); return $latlog->toString(); } } // ================================================================== RefEll class RefEll { public $maj; public $min; public $ecc; /** * Create a new RefEll object to represent a reference ellipsoid * * @param float|integer maj the major axis * @param float|integer min the minor axis */ public function __construct($maj, $min) { $this->maj = $maj; $this->min = $min; $this->ecc = ( $maj * $maj - $min * $min ) / ($maj * $maj); } } // ================================================== Mathematical Functions function sinSquared($x) { return sin($x) * sin($x); } function cosSquared($x) { return cos($x) * cos($x); } function tanSquared($x) { return tan($x) * tan($x); } function sec($x) { return 1.0 / cos($x); } /** * Take a string formatted as a six-figure OS grid reference (e.g. * "TG514131") and return a reference to an OSRef object that represents * that grid reference. The first character must be H, N, S, O or T. * The second character can be any uppercase character from A through Z * excluding I. * * @param string $ref * @return string * @since 2.1 */ function getOSRefFromSixFigureReference($ref) { $char1 = substr($ref, 0, 1); $char2 = substr($ref, 1, 1); $east = substr($ref, 2, 3) * 100; $north = substr($ref, 5, 3) * 100; switch ($char1) { case 'H': $north += 1000000; break; case 'N': $north += 500000; break; case 'O': $north += 500000; $east += 500000; break; case 'T': $east += 500000; break; } $char2ord = ord($char2); if ($char2ord > 73) $char2ord--; // Adjust for no I $nx = (($char2ord - 65) % 5) * 100000; $ny = (4 - floor(($char2ord - 65) / 5)) * 100000; $osref = new OSRef($east + $nx, $north + $ny); return $osref->toString(); } /** * Work out the UTM latitude zone from the latitude * * @param float|integer latitude * @return */ function getUTMLatitudeZoneLetter($latitude) { if ((84 >= $latitude) && ($latitude >= 72)) return "X"; else if (( 72 > $latitude) && ($latitude >= 64)) return "W"; else if (( 64 > $latitude) && ($latitude >= 56)) return "V"; else if (( 56 > $latitude) && ($latitude >= 48)) return "U"; else if (( 48 > $latitude) && ($latitude >= 40)) return "T"; else if (( 40 > $latitude) && ($latitude >= 32)) return "S"; else if (( 32 > $latitude) && ($latitude >= 24)) return "R"; else if (( 24 > $latitude) && ($latitude >= 16)) return "Q"; else if (( 16 > $latitude) && ($latitude >= 8)) return "P"; else if (( 8 > $latitude) && ($latitude >= 0)) return "N"; else if (( 0 > $latitude) && ($latitude >= -8)) return "M"; else if (( -8 > $latitude) && ($latitude >= -16)) return "L"; else if ((-16 > $latitude) && ($latitude >= -24)) return "K"; else if ((-24 > $latitude) && ($latitude >= -32)) return "J"; else if ((-32 > $latitude) && ($latitude >= -40)) return "H"; else if ((-40 > $latitude) && ($latitude >= -48)) return "G"; else if ((-48 > $latitude) && ($latitude >= -56)) return "F"; else if ((-56 > $latitude) && ($latitude >= -64)) return "E"; else if ((-64 > $latitude) && ($latitude >= -72)) return "D"; else if ((-72 > $latitude) && ($latitude >= -80)) return "C"; else return 'Z'; }