mirror of
https://github.com/TheAlgorithms/Python.git
synced 2026-09-28 13:33:11 +08:00
* Add radar target coordinate calculation * Move radar target calculation to geodesy folder * Add Wikipedia references to comply with PR template * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix E501 line too long in docstring * Rename variables to descriptive names to pass algorithms-keeper * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix doctest expected value to pass build tests --------- Co-authored-by: deniztunccekic <deniztunccekic@example.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
181 lines
5.9 KiB
Python
181 lines
5.9 KiB
Python
"""
|
|
This module provides functions to convert between Geodetic coordinates and
|
|
Earth-Centered, Earth-Fixed (ECEF) Cartesian coordinates, as well as calculating
|
|
target coordinates based on radar measurements.
|
|
|
|
Reference:
|
|
- https://en.wikipedia.org/wiki/Geographic_coordinate_conversion
|
|
- https://en.wikipedia.org/wiki/Local_tangent_plane_coordinates
|
|
"""
|
|
|
|
import math
|
|
|
|
# WGS84 Ellipsoid Constants
|
|
WGS84_A = 6378137.0 # Semi-major axis in meters
|
|
WGS84_B = 6356752.314245 # Semi-minor axis in meters
|
|
WGS84_E_SQ = 1.0 - (WGS84_B**2 / WGS84_A**2) # First eccentricity squared
|
|
WGS84_EP_SQ = (WGS84_A**2 - WGS84_B**2) / WGS84_B**2 # Second eccentricity squared
|
|
|
|
|
|
def geodetic_to_ecef(
|
|
lat_deg: float, lon_deg: float, alt_m: float
|
|
) -> tuple[float, float, float]:
|
|
"""
|
|
Converts Geodetic coordinates (Latitude, Longitude, Altitude) to
|
|
Earth-Centered, Earth-Fixed (ECEF) Cartesian coordinates.
|
|
|
|
>>> x, y, z = geodetic_to_ecef(0.0, 0.0, 0.0)
|
|
>>> round(x, 2), round(y, 2), round(z, 2)
|
|
(6378137.0, 0.0, 0.0)
|
|
>>> x, y, z = geodetic_to_ecef(90.0, 0.0, 0.0)
|
|
>>> round(x, 2), round(y, 2), round(z, 2)
|
|
(0.0, 0.0, 6356752.31)
|
|
"""
|
|
lat_rad = math.radians(lat_deg)
|
|
lon_rad = math.radians(lon_deg)
|
|
|
|
sin_lat = math.sin(lat_rad)
|
|
cos_lat = math.cos(lat_rad)
|
|
|
|
# N is the prime vertical radius of curvature
|
|
n_radius = WGS84_A / math.sqrt(1.0 - WGS84_E_SQ * sin_lat**2)
|
|
|
|
# Calculate ECEF X, Y, Z
|
|
x = (n_radius + alt_m) * cos_lat * math.cos(lon_rad)
|
|
y = (n_radius + alt_m) * cos_lat * math.sin(lon_rad)
|
|
z = (n_radius * (1.0 - WGS84_E_SQ) + alt_m) * sin_lat
|
|
|
|
return x, y, z
|
|
|
|
|
|
def ecef_to_geodetic(
|
|
x_ecef: float, y_ecef: float, z_ecef: float
|
|
) -> tuple[float, float, float]:
|
|
"""
|
|
Converts Earth-Centered, Earth-Fixed (ECEF) coordinates to
|
|
Geodetic coordinates (Latitude, Longitude, Altitude) using Bowring's method.
|
|
|
|
>>> lat, lon, alt = ecef_to_geodetic(6378137.0, 0.0, 0.0)
|
|
>>> round(lat, 2), round(lon, 2), round(alt, 2)
|
|
(0.0, 0.0, 0.0)
|
|
>>> lat, lon, alt = ecef_to_geodetic(0.0, 0.0, 6356752.314245)
|
|
>>> round(lat, 2), round(lon, 2), round(alt, 2)
|
|
(90.0, 0.0, 0.0)
|
|
"""
|
|
p = math.sqrt(x_ecef**2 + y_ecef**2)
|
|
|
|
# Handle the special case where the point is exactly at the poles
|
|
if p == 0:
|
|
lon_deg = 0.0
|
|
lat_deg = 90.0 if z_ecef > 0 else -90.0
|
|
alt_m = abs(z_ecef) - WGS84_B
|
|
return lat_deg, lon_deg, alt_m
|
|
|
|
theta = math.atan2(z_ecef * WGS84_A, p * WGS84_B)
|
|
|
|
sin_theta = math.sin(theta)
|
|
cos_theta = math.cos(theta)
|
|
|
|
# Calculate exact latitude and longitude
|
|
lon_rad = math.atan2(y_ecef, x_ecef)
|
|
lat_rad = math.atan2(
|
|
z_ecef + WGS84_EP_SQ * WGS84_B * sin_theta**3,
|
|
p - WGS84_E_SQ * WGS84_A * cos_theta**3,
|
|
)
|
|
|
|
sin_lat = math.sin(lat_rad)
|
|
|
|
# Recalculate prime vertical radius to find altitude
|
|
n_radius = WGS84_A / math.sqrt(1.0 - WGS84_E_SQ * sin_lat**2)
|
|
|
|
alt_m = (p / math.cos(lat_rad)) - n_radius
|
|
|
|
return math.degrees(lat_rad), math.degrees(lon_rad), alt_m
|
|
|
|
|
|
def enu_to_ecef(
|
|
east: float, north: float, up: float, ref_lat_deg: float, ref_lon_deg: float
|
|
) -> tuple[float, float, float]:
|
|
"""
|
|
Rotates East-North-Up (ENU) offset coordinates to ECEF offset coordinates,
|
|
based on the reference (Radar) latitude and longitude.
|
|
|
|
>>> dx, dy, dz = enu_to_ecef(100.0, 200.0, 50.0, 0.0, 0.0)
|
|
>>> round(dx, 2), round(dy, 2), round(dz, 2)
|
|
(50.0, 100.0, 200.0)
|
|
"""
|
|
lat_rad = math.radians(ref_lat_deg)
|
|
lon_rad = math.radians(ref_lon_deg)
|
|
|
|
sin_lat = math.sin(lat_rad)
|
|
cos_lat = math.cos(lat_rad)
|
|
sin_lon = math.sin(lon_rad)
|
|
cos_lon = math.cos(lon_rad)
|
|
|
|
# Rotation matrix components for ENU to ECEF
|
|
dx = -sin_lon * east - sin_lat * cos_lon * north + cos_lat * cos_lon * up
|
|
dy = cos_lon * east - sin_lat * sin_lon * north + cos_lat * sin_lon * up
|
|
dz = cos_lat * north + sin_lat * up
|
|
|
|
return dx, dy, dz
|
|
|
|
|
|
def calculate_target_coordinates(
|
|
radar_lat: float,
|
|
radar_lon: float,
|
|
radar_alt: float,
|
|
azimuth_deg: float,
|
|
range_m: float,
|
|
elevation_deg: float = 0.0,
|
|
) -> tuple[float, float, float]:
|
|
"""
|
|
Main function to calculate target (ship) coordinates from radar measurements.
|
|
|
|
Parameters:
|
|
radar_lat (float): Radar latitude in degrees
|
|
radar_lon (float): Radar longitude in degrees
|
|
radar_alt (float): Radar altitude above sea level in meters
|
|
azimuth_deg (float): True bearing to the target (0 is North, 90 is East)
|
|
range_m (float): Direct line-of-sight distance to the target in meters
|
|
elevation_deg (float): Antenna elevation angle in degrees
|
|
(default 0 for surface ships)
|
|
|
|
Returns:
|
|
tuple: (Target Latitude, Target Longitude, Target Altitude)
|
|
|
|
>>> lat, lon, alt = calculate_target_coordinates(0.0, 0.0, 0.0, 90.0, 111319.5)
|
|
>>> round(lat, 1), round(lon, 1), round(alt, 1)
|
|
(0.0, 1.0, 971.4)
|
|
"""
|
|
# Step 1: Convert Radar polar measurements to Local ENU Cartesian coordinates
|
|
az_rad = math.radians(azimuth_deg)
|
|
el_rad = math.radians(elevation_deg)
|
|
|
|
# Standard spherical to cartesian for ENU
|
|
# North is aligned with 0 degrees Azimuth, East is 90 degrees
|
|
east = range_m * math.cos(el_rad) * math.sin(az_rad)
|
|
north = range_m * math.cos(el_rad) * math.cos(az_rad)
|
|
up = range_m * math.sin(el_rad)
|
|
|
|
# Step 2: Get absolute ECEF position of the Radar
|
|
radar_x, radar_y, radar_z = geodetic_to_ecef(radar_lat, radar_lon, radar_alt)
|
|
|
|
# Step 3: Convert the Local ENU offsets to ECEF offsets
|
|
dx, dy, dz = enu_to_ecef(east, north, up, radar_lat, radar_lon)
|
|
|
|
# Step 4: Add offsets to the Radar's ECEF coordinates to find Target ECEF
|
|
target_x = radar_x + dx
|
|
target_y = radar_y + dy
|
|
target_z = radar_z + dz
|
|
|
|
# Step 5: Convert Target ECEF back to Geodetic coordinates
|
|
target_lat, target_lon, target_alt = ecef_to_geodetic(target_x, target_y, target_z)
|
|
|
|
return target_lat, target_lon, target_alt
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import doctest
|
|
|
|
doctest.testmod()
|