Skip to content

Commit 33a4141

Browse files
committed
feature #2551 [Map] Add new helpers: DistanceUnit, DistanceCalculator, CoordinateUtils (smnandre)
This PR was squashed before being merged into the 2.x branch. Discussion ---------- [Map] Add new helpers: DistanceUnit, DistanceCalculator, CoordinateUtils | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Issues | Fix # | License | MIT To ease the work on distances while managing maps, this PR introduce some new tools - `DistanceUnit` enum, that contains the most common distance units and their conversion ratios - A `DistanceCalculatorInterface`, and 3 implementations (most commonly used formulas) - A `DistanceCalculator` final service, that defaults to Vincenty formula and Meter unit - A `CoordinateUtils` helper to convert DMS coordinates (`48° 7′ 3″ N`) in decimal ones (`48.1175 N`) Also, added `getLatitude()` and `getLongitude()` on `Point`, avoiding the use of toArray() Commits ------- 381bc6e [Map] Add new helpers: DistanceUnit, DistanceCalculator, CoordinateUtils
2 parents 266e1a8 + 381bc6e commit 33a4141

13 files changed

+619
-0
lines changed

src/Map/CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# CHANGELOG
22

3+
## 2.23
4+
5+
- Add `DistanceUnit` to represent distance units (`m`, `km`, `miles`, `nmi`) and
6+
ease conversion between units.
7+
- Add `DistanceCalculatorInterface` interface and three implementations:
8+
`HaversineDistanceCalculator`, `SphericalCosineDistanceCalculator` and `VincentyDistanceCalculator`.
9+
- Add `CoordinateUtils` helper, to convert decimal coordinates (`43.2109`) in DMS (`56° 78' 90"`)
10+
311
## 2.22
412

513
- Add method `Symfony\UX\Map\Renderer\AbstractRenderer::tapOptions()`, to allow Renderer to modify options before rendering a Map.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\Map\Distance;
13+
14+
use Symfony\UX\Map\Point;
15+
16+
/**
17+
* @author Simon André <[email protected]>
18+
*/
19+
final readonly class DistanceCalculator implements DistanceCalculatorInterface
20+
{
21+
public function __construct(
22+
private DistanceCalculatorInterface $calculator = new VincentyDistanceCalculator(),
23+
private DistanceUnit $unit = DistanceUnit::Meter,
24+
) {
25+
}
26+
27+
public function calculateDistance(Point $point1, Point $point2): float
28+
{
29+
return $this->calculator->calculateDistance($point1, $point2)
30+
* $this->unit->getConversionFactor();
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\Map\Distance;
13+
14+
use Symfony\UX\Map\Point;
15+
16+
/**
17+
* Interface for distance calculators.
18+
*
19+
* @author Simon André <[email protected]>
20+
*/
21+
interface DistanceCalculatorInterface
22+
{
23+
/**
24+
* Returns the distance between two points given their coordinates.
25+
*
26+
* @return float the distance between the two points, in meters
27+
*/
28+
public function calculateDistance(Point $point1, Point $point2): float;
29+
}

src/Map/src/Distance/DistanceUnit.php

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\Map\Distance;
13+
14+
/**
15+
* Represents a distance unit used in mapping and geospatial calculations.
16+
*
17+
* This enum defines common units for measuring distances. Each unit has an associated
18+
* conversion factor which is used to convert a value from meters to that unit.
19+
*
20+
* @author Simon André <[email protected]>
21+
*/
22+
enum DistanceUnit: string
23+
{
24+
/**
25+
* The "meter" unit.
26+
*
27+
* This is the International System of Units (SI) base unit for length.
28+
*/
29+
case Meter = 'm';
30+
31+
/**
32+
* The "kilometer" unit.
33+
*
34+
* This unit is commonly used for longer distances.
35+
*/
36+
case Kilometer = 'km';
37+
38+
/**
39+
* The "mile" unit.
40+
*
41+
* This unit is widely used in the United States.
42+
*/
43+
case Mile = 'mi';
44+
45+
/**
46+
* The "nautical mile" unit.
47+
*
48+
* This unit is typically used in navigation.
49+
*/
50+
case NauticalMile = 'nmi';
51+
52+
/**
53+
* Returns the conversion factor to convert this unit to meters.
54+
*/
55+
public function getConversionFactor(): float
56+
{
57+
return match ($this) {
58+
self::Meter => 1.0,
59+
self::Kilometer => 0.001,
60+
self::Mile => 0.000621371,
61+
self::NauticalMile => 0.000539957,
62+
};
63+
}
64+
65+
/**
66+
* Returns the conversion factor to convert this unit to another unit.
67+
*/
68+
public function getConversionFactorTo(DistanceUnit $unit): float
69+
{
70+
return $this->getConversionFactor() / $unit->getConversionFactor();
71+
}
72+
73+
/**
74+
* Returns the conversion factor to convert another unit to this unit.
75+
*/
76+
public function getConversionFactorFrom(DistanceUnit $unit): float
77+
{
78+
return $unit->getConversionFactor() / $this->getConversionFactor();
79+
}
80+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\Map\Distance;
13+
14+
use Symfony\UX\Map\Point;
15+
16+
/**
17+
* Haversine formula-based distance calculator.
18+
*
19+
* This calculator is accurate but slower than the spherical cosine formula.
20+
*
21+
* @author Simon André <[email protected]>
22+
*/
23+
final readonly class HaversineDistanceCalculator implements DistanceCalculatorInterface
24+
{
25+
/**
26+
* @const float The Earth's radius in meters.
27+
*/
28+
private const EARTH_RADIUS = 6371000.0;
29+
30+
public function calculateDistance(Point $point1, Point $point2): float
31+
{
32+
$lat1Rad = deg2rad($point1->getLatitude());
33+
$lat2Rad = deg2rad($point2->getLatitude());
34+
$deltaLat = deg2rad($point2->getLatitude() - $point1->getLatitude());
35+
$deltaLng = deg2rad($point2->getLongitude() - $point1->getLongitude());
36+
37+
$a = sin($deltaLat / 2) ** 2 + cos($lat1Rad) * cos($lat2Rad) * sin($deltaLng / 2) ** 2;
38+
$c = 2 * asin(min(1.0, sqrt($a)));
39+
40+
return self::EARTH_RADIUS * $c;
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\Map\Distance;
13+
14+
use Symfony\UX\Map\Point;
15+
16+
/**
17+
* Sphere-based distance calculator using the cosine of the spherical distance.
18+
*
19+
* This calculator is faster than the Haversine formula, but less accurate.
20+
*
21+
* @author Simon André <[email protected]>
22+
*/
23+
final readonly class SphericalCosineDistanceCalculator implements DistanceCalculatorInterface
24+
{
25+
/**
26+
* @const float The Earth's radius in meters.
27+
*/
28+
private const EARTH_RADIUS = 6371000.0;
29+
30+
public function calculateDistance(Point $point1, Point $point2): float
31+
{
32+
$lat1Rad = deg2rad($point1->getLatitude());
33+
$lat2Rad = deg2rad($point2->getLatitude());
34+
$lng1Rad = deg2rad($point1->getLongitude());
35+
$lng2Rad = deg2rad($point2->getLongitude());
36+
37+
$cosDistance = sin($lat1Rad) * sin($lat2Rad) + cos($lat1Rad) * cos($lat2Rad) * cos($lng2Rad - $lng1Rad);
38+
39+
// Correct for floating-point errors.
40+
$cosDistance = min(1.0, max(-1.0, $cosDistance));
41+
$angle = acos($cosDistance);
42+
43+
return self::EARTH_RADIUS * $angle;
44+
}
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\Map\Distance;
13+
14+
use Symfony\UX\Map\Point;
15+
16+
/**
17+
* Vincenty formula-based distance calculator.
18+
*
19+
* This calculator is more accurate than the Haversine formula, but slower.
20+
*
21+
* @author Simon André <[email protected]>
22+
*/
23+
final readonly class VincentyDistanceCalculator implements DistanceCalculatorInterface
24+
{
25+
/**
26+
* WS-84 ellipsoid parameters.
27+
*/
28+
// Major Axis in meters
29+
private const A = 6378137.0;
30+
// Flattening
31+
private const F = 1 / 298.257223563;
32+
// Minor Axis in meters
33+
private const B = 6356752.314245;
34+
35+
public function calculateDistance(Point $point1, Point $point2): float
36+
{
37+
$phi1 = deg2rad($point1->getLatitude());
38+
$phi2 = deg2rad($point2->getLatitude());
39+
$lambda1 = deg2rad($point1->getLongitude());
40+
$lambda2 = deg2rad($point2->getLongitude());
41+
42+
$L = $lambda2 - $lambda1;
43+
$U1 = atan((1 - self::F) * tan($phi1));
44+
$U2 = atan((1 - self::F) * tan($phi2));
45+
$sinU1 = sin($U1);
46+
$cosU1 = cos($U1);
47+
$sinU2 = sin($U2);
48+
$cosU2 = cos($U2);
49+
50+
$lambda = $L;
51+
$iterLimit = 100;
52+
do {
53+
$sinLambda = sin($lambda);
54+
$cosLambda = cos($lambda);
55+
$sinSigma = sqrt(($cosU2 * $sinLambda) ** 2
56+
+ ($cosU1 * $sinU2 - $sinU1 * $cosU2 * $cosLambda) ** 2);
57+
58+
if (0.0 === $sinSigma) {
59+
return 0.0;
60+
}
61+
62+
$cosSigma = $sinU1 * $sinU2 + $cosU1 * $cosU2 * $cosLambda;
63+
$sigma = atan2($sinSigma, $cosSigma);
64+
$sinAlpha = $cosU1 * $cosU2 * $sinLambda / $sinSigma;
65+
$cosSqAlpha = 1 - $sinAlpha * $sinAlpha;
66+
$cos2SigmaM = (0.0 === $cosSqAlpha) ? 0.0 : $cosSigma - 2 * $sinU1 * $sinU2 / $cosSqAlpha;
67+
$C = self::F / 16 * $cosSqAlpha * (4 + self::F * (4 - 3 * $cosSqAlpha));
68+
69+
$lambdaPrev = $lambda;
70+
$lambda = $L + (1 - $C) * self::F * $sinAlpha
71+
* ($sigma + $C * $sinSigma * ($cos2SigmaM + $C * $cosSigma * (-1 + 2 * $cos2SigmaM * $cos2SigmaM)));
72+
} while (abs($lambda - $lambdaPrev) > 1e-12 && --$iterLimit > 0);
73+
74+
if (0 === $iterLimit) {
75+
throw new \RuntimeException('Vincenty formula failed to converge.');
76+
}
77+
78+
$uSq = $cosSqAlpha * (self::A * self::A - self::B * self::B) / (self::B * self::B);
79+
$Acoeff = 1 + $uSq / 16384 * (4096 + $uSq * (-768 + $uSq * (320 - 175 * $uSq)));
80+
$Bcoeff = $uSq / 1024 * (256 + $uSq * (-128 + $uSq * (74 - 47 * $uSq)));
81+
$deltaSigma = $Bcoeff * $sinSigma * ($cos2SigmaM + $Bcoeff / 4 * ($cosSigma * (-1 + 2 * $cos2SigmaM * $cos2SigmaM)
82+
- $Bcoeff / 6 * $cos2SigmaM * (-3 + 4 * $sinSigma * $sinSigma) * (-3 + 4 * $cos2SigmaM * $cos2SigmaM)));
83+
$distance = self::B * $Acoeff * ($sigma - $deltaSigma);
84+
85+
return $distance;
86+
}
87+
}

src/Map/src/Point.php

+10
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ public function __construct(
3333
}
3434
}
3535

36+
public function getLatitude(): float
37+
{
38+
return $this->latitude;
39+
}
40+
41+
public function getLongitude(): float
42+
{
43+
return $this->longitude;
44+
}
45+
3646
/**
3747
* @return array{lat: float, lng: float}
3848
*/

0 commit comments

Comments
 (0)