from typing import Optional
import matplotlib.pyplot as plt
import numpy as np
from astropy import units as u
from matplotlib.axes import Axes
try:
from .._core import compute_ground_track, ecef_to_geodetic
from ..twobody import Orbit
except ImportError:
Orbit = None
ecef_to_geodetic = None
compute_ground_track = None
def plot_ground_track(
orbit: "Orbit",
duration: float,
dt: float = 60.0,
ax: Optional[Axes] = None,
show_map: bool = True,
color: Optional[str] = None,
label: Optional[str] = None,
**kwargs,
) -> Axes:
if ecef_to_geodetic is None:
raise ImportError(
"Ground track functions not available. " "Make sure astrora._core is properly compiled."
)
if ax is None:
fig, ax = plt.subplots(figsize=(14, 8))
if hasattr(duration, "unit"):
duration_seconds = duration.to(u.s).value
else:
duration_seconds = duration
times = np.arange(0, duration_seconds, dt)
positions, _ = orbit.sample(times)
if hasattr(positions, "unit"):
positions_km = positions.to(u.km).value
else:
positions_km = positions / 1000.0
latitudes = []
longitudes = []
for pos in positions_km:
result = ecef_to_geodetic(pos * 1000.0) latitudes.append(result["latitude_deg"])
longitudes.append(result["longitude_deg"])
latitudes = np.array(latitudes)
longitudes = np.array(longitudes)
lon_diff = np.diff(longitudes)
breaks = np.where(np.abs(lon_diff) > 180)[0]
plot_kwargs = {}
if color is not None:
plot_kwargs["color"] = color
if label is not None:
plot_kwargs["label"] = label
plot_kwargs.update(kwargs)
if len(breaks) == 0:
ax.plot(longitudes, latitudes, **plot_kwargs)
else:
breaks = np.concatenate(([0], breaks + 1, [len(longitudes)]))
for i in range(len(breaks) - 1):
start, end = breaks[i], breaks[i + 1]
segment_kwargs = plot_kwargs.copy()
if i > 0:
segment_kwargs.pop("label", None)
ax.plot(longitudes[start:end], latitudes[start:end], **segment_kwargs)
ax.plot(longitudes[0], latitudes[0], "go", markersize=8, label="Start", zorder=10)
ax.plot(longitudes[-1], latitudes[-1], "ro", markersize=8, label="End", zorder=10)
ax.set_xlim(-180, 180)
ax.set_ylim(-90, 90)
ax.set_xlabel("Longitude (°)", fontsize=12)
ax.set_ylabel("Latitude (°)", fontsize=12)
ax.set_title("Satellite Ground Track", fontsize=14, fontweight="bold")
ax.grid(True, alpha=0.3)
ax.set_aspect("equal", adjustable="box")
if show_map:
try:
import cartopy.crs as ccrs
import cartopy.feature as cfeature
ax.set_xticks(np.arange(-180, 181, 30))
ax.set_yticks(np.arange(-90, 91, 30))
ax.axhline(y=0, color="k", linestyle="--", alpha=0.3, linewidth=0.5)
ax.axvline(x=0, color="k", linestyle="--", alpha=0.3, linewidth=0.5)
except ImportError:
ax.set_xticks(np.arange(-180, 181, 30))
ax.set_yticks(np.arange(-90, 91, 30))
ax.axhline(y=0, color="k", linestyle="--", alpha=0.3, linewidth=0.5)
ax.axvline(x=0, color="k", linestyle="--", alpha=0.3, linewidth=0.5)
ax.legend(loc="upper right")
plt.tight_layout()
return ax
def plot_ground_track_3d(
orbit: "Orbit", duration: float, dt: float = 60.0, show_earth: bool = True, **kwargs
) -> None:
try:
import plotly.graph_objects as go
except ImportError:
raise ImportError(
"Plotly is required for 3D ground tracks. " "Install with: pip install plotly"
)
if hasattr(duration, "unit"):
duration_seconds = duration.to(u.s).value
else:
duration_seconds = duration
times = np.arange(0, duration_seconds, dt)
positions, _ = orbit.sample(times)
if hasattr(positions, "unit"):
positions_km = positions.to(u.km).value
else:
positions_km = positions / 1000.0
fig = go.Figure()
if show_earth:
R_earth = 6378.137
theta = np.linspace(0, 2 * np.pi, 50)
phi = np.linspace(0, np.pi, 50)
x = R_earth * np.outer(np.cos(theta), np.sin(phi))
y = R_earth * np.outer(np.sin(theta), np.sin(phi))
z = R_earth * np.outer(np.ones(np.size(theta)), np.cos(phi))
fig.add_trace(
go.Surface(
x=x,
y=y,
z=z,
colorscale=[[0, "#4d69bb"], [1, "#4d69bb"]],
showscale=False,
name="Earth",
hoverinfo="name",
opacity=0.9,
)
)
fig.add_trace(
go.Scatter3d(
x=positions_km[:, 0],
y=positions_km[:, 1],
z=positions_km[:, 2],
mode="lines",
line=dict(color="red", width=3),
name="Orbit",
)
)
fig.add_trace(
go.Scatter3d(
x=[positions_km[0, 0]],
y=[positions_km[0, 1]],
z=[positions_km[0, 2]],
mode="markers",
marker=dict(size=8, color="green"),
name="Start",
)
)
fig.add_trace(
go.Scatter3d(
x=[positions_km[-1, 0]],
y=[positions_km[-1, 1]],
z=[positions_km[-1, 2]],
mode="markers",
marker=dict(size=8, color="red"),
name="End",
)
)
fig.update_layout(
scene=dict(
xaxis_title="x (km)",
yaxis_title="y (km)",
zaxis_title="z (km)",
aspectmode="data",
),
title="3D Ground Track Visualization",
showlegend=True,
)
fig.show()