/** * testCalculateDestinationForBearingAndDistanceReturnsExpectedValue * * @param LatLong $latLong The starting point. * @param float $bearing The initial bearing. * @param float $distance The distance in metres to travel. * @param LatLong $expected The expected destination. * * @dataProvider getLatLongWithInitialBearingDistanceAndDestination */ public function testCalculateDestinationForBearingAndDistanceReturnsExpectedValue(LatLong $latLong, $bearing, $distance, LatLong $expected) { $actual = $latLong->calculateDestinationForBearingAndDistance($bearing, $distance); $tolerance = 0.3; $this->assertEqualsWithinTolerance($expected->getLatitude(), $actual->getLatitude(), $tolerance); $this->assertEqualsWithinTolerance($expected->getLongitude(), $actual->getLongitude(), $tolerance); $this->assertEquals($latLong->getHeight(), $actual->getHeight()); // Height remains constant. $this->assertEquals($expected->getDatum(), $actual->getDatum()); }
/** * Calculate the distance in metres to another LatLong coordinate. * * This is a relatively expensive calculation to perform. It treats the Earth as an ellipsoid of the dimensions * given for this LatLong's Datum's Ellipsoid so is more accurate than the Spherical law of cosines. * * @param LatLong $destination The coordinate to measure the distance to. * * @return float|null The distance in metres, or null if there was a problem. * * @throws InvalidArgumentException If the datum of this object does not match that of the $destination LatLong. */ public function calculateDistanceVincenty(LatLong $destination) { if ($destination->getDatum() != $this->datum) { throw new InvalidArgumentException('Datums must match to calculate distance.'); } $ellipsoid = $this->datum->getEllipsoid(); $a = $ellipsoid->getSemiMajorAxisMetres(); $b = $ellipsoid->getSemiMinorAxisMetres(); $f = $ellipsoid->getInverseFlattening(); $longDiffRad = deg2rad($destination->getLongitude() - $this->getLongitude()); $reducedLat = atan((1 - $f) * tan($this->getLatitudeRadians())); $reducedLatDest = atan((1 - $f) * tan($destination->getLatitudeRadians())); $sinReducedLat = sin($reducedLat); $cosReducedLat = cos($reducedLat); $sinReducedLatDest = sin($reducedLatDest); $cosReducedLatDest = cos($reducedLatDest); $lambda = $longDiffRad; $maxIterations = 100; do { $sinLambda = sin($lambda); $cosLambda = cos($lambda); $sinSigma = sqrt(pow($cosReducedLatDest * $sinLambda, 2) + pow($cosReducedLat * $sinReducedLatDest - $sinReducedLat * $cosReducedLatDest * $cosLambda, 2)); // Return 0 if the points are coincident. if ($sinSigma === 0.0) { return 0.0; } $cosSigma = $sinReducedLat * $sinReducedLatDest + $cosReducedLat * $cosReducedLatDest * $cosLambda; $sigma = atan2($sinSigma, $cosSigma); $sinAlpha = $cosReducedLat * $cosReducedLatDest * $sinLambda / $sinSigma; $cosSqAlpha = 1 - pow($sinAlpha, 2); $cos2SigmaM = 0; // At the equator, $cosSqAlpha === 0. if ($cosSqAlpha !== 0) { $cos2SigmaM = $cosSigma - 2 * $sinReducedLat * $sinReducedLatDest / $cosSqAlpha; } $c = $f / 16 * $cosSqAlpha * (4 + $f * (4 - 3 * $cosSqAlpha)); $lambdaP = $lambda; $lambda = $longDiffRad + (1 - $c) * $f * $sinAlpha * ($sigma + $c * $sinSigma * ($cos2SigmaM + $c * $cosSigma * (-1 + 2 * $cos2SigmaM * $cos2SigmaM))); } while (abs($lambda - $lambdaP) > '1e-12' && --$maxIterations > 0); if ($maxIterations === 0) { return null; // Formula failed to converge. } $uSq = $cosSqAlpha * ($a * $a - $b * $b) / ($b * $b); $A = 1 + $uSq / 16384 * (4096 + $uSq * (-768 + $uSq * (320 - 175 * $uSq))); $B = $uSq / 1024 * (256 + $uSq * (-128 + $uSq * (74 - 47 * $uSq))); $deltaSigma = $B * $sinSigma * ($cos2SigmaM + $B / 4 * ($cosSigma * (-1 + 2 * $cos2SigmaM * $cos2SigmaM) - $B / 6 * $cos2SigmaM * (-3 + 4 * $sinSigma * $sinSigma) * (-3 + 4 * $cos2SigmaM * $cos2SigmaM))); $s = $b * $A * ($sigma - $deltaSigma); return $s; }