starplot 0.20.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. starplot/__init__.py +59 -0
  2. starplot/callables.py +176 -0
  3. starplot/cli.py +29 -0
  4. starplot/config.py +80 -0
  5. starplot/coordinates.py +7 -0
  6. starplot/data/__init__.py +22 -0
  7. starplot/data/catalogs.py +356 -0
  8. starplot/data/constellations.py +71 -0
  9. starplot/data/db.py +33 -0
  10. starplot/data/dsos.py +85 -0
  11. starplot/data/ecliptic.py +201 -0
  12. starplot/data/library/constellation_names.parquet +0 -0
  13. starplot/data/library/dso_names.parquet +0 -0
  14. starplot/data/library/readme.md +1 -0
  15. starplot/data/library/star_designations.parquet +0 -0
  16. starplot/data/stars.py +79 -0
  17. starplot/data/translations.py +408 -0
  18. starplot/data/utils.py +35 -0
  19. starplot/geometry.py +319 -0
  20. starplot/mixins.py +360 -0
  21. starplot/models/__init__.py +21 -0
  22. starplot/models/base.py +77 -0
  23. starplot/models/comet.py +302 -0
  24. starplot/models/constellation.py +151 -0
  25. starplot/models/dso.py +317 -0
  26. starplot/models/milky_way.py +30 -0
  27. starplot/models/moon.py +130 -0
  28. starplot/models/objects.py +29 -0
  29. starplot/models/observer.py +125 -0
  30. starplot/models/optics.py +342 -0
  31. starplot/models/planet.py +137 -0
  32. starplot/models/satellite.py +138 -0
  33. starplot/models/star.py +253 -0
  34. starplot/models/sun.py +62 -0
  35. starplot/plots/__init__.py +7 -0
  36. starplot/plots/base.py +1023 -0
  37. starplot/plots/galaxy.py +372 -0
  38. starplot/plots/horizon.py +543 -0
  39. starplot/plots/map.py +515 -0
  40. starplot/plots/optic.py +469 -0
  41. starplot/plots/zenith.py +217 -0
  42. starplot/plotters/__init__.py +9 -0
  43. starplot/plotters/arrow.py +174 -0
  44. starplot/plotters/constellations.py +298 -0
  45. starplot/plotters/debug.py +21 -0
  46. starplot/plotters/dsos.py +294 -0
  47. starplot/plotters/experimental.py +722 -0
  48. starplot/plotters/gradients.py +153 -0
  49. starplot/plotters/legend.py +253 -0
  50. starplot/plotters/milkyway.py +51 -0
  51. starplot/plotters/stars.py +319 -0
  52. starplot/plotters/text.py +802 -0
  53. starplot/profile.py +16 -0
  54. starplot/projections.py +184 -0
  55. starplot/styles/__init__.py +6 -0
  56. starplot/styles/base.py +1344 -0
  57. starplot/styles/ext/antique.yml +175 -0
  58. starplot/styles/ext/blue_dark.yml +163 -0
  59. starplot/styles/ext/blue_gold.yml +147 -0
  60. starplot/styles/ext/blue_light.yml +123 -0
  61. starplot/styles/ext/blue_medium.yml +142 -0
  62. starplot/styles/ext/blue_night.yml +185 -0
  63. starplot/styles/ext/cb_wong.yml +124 -0
  64. starplot/styles/ext/color_print.yml +111 -0
  65. starplot/styles/ext/gradient_presets.yml +158 -0
  66. starplot/styles/ext/grayscale.yml +94 -0
  67. starplot/styles/ext/grayscale_dark.yml +136 -0
  68. starplot/styles/ext/map.yml +12 -0
  69. starplot/styles/ext/nord.yml +158 -0
  70. starplot/styles/ext/optic.yml +20 -0
  71. starplot/styles/ext/publication.yml +8 -0
  72. starplot/styles/extensions.py +129 -0
  73. starplot/styles/fonts-library/gfs-didot/DESCRIPTION.en_us.html +9 -0
  74. starplot/styles/fonts-library/gfs-didot/GFSDidot-Regular.ttf +0 -0
  75. starplot/styles/fonts-library/gfs-didot/METADATA.pb +16 -0
  76. starplot/styles/fonts-library/gfs-didot/OFL.txt +94 -0
  77. starplot/styles/fonts-library/inter/Inter-Bold.ttf +0 -0
  78. starplot/styles/fonts-library/inter/Inter-BoldItalic.ttf +0 -0
  79. starplot/styles/fonts-library/inter/Inter-ExtraBold.ttf +0 -0
  80. starplot/styles/fonts-library/inter/Inter-ExtraLight.ttf +0 -0
  81. starplot/styles/fonts-library/inter/Inter-ExtraLightItalic.ttf +0 -0
  82. starplot/styles/fonts-library/inter/Inter-Italic.ttf +0 -0
  83. starplot/styles/fonts-library/inter/Inter-Light.ttf +0 -0
  84. starplot/styles/fonts-library/inter/Inter-LightItalic.ttf +0 -0
  85. starplot/styles/fonts-library/inter/Inter-Regular.ttf +0 -0
  86. starplot/styles/fonts-library/inter/Inter-SemiBold.ttf +0 -0
  87. starplot/styles/fonts-library/inter/Inter-SemiBoldItalic.ttf +0 -0
  88. starplot/styles/fonts-library/inter/LICENSE.txt +92 -0
  89. starplot/styles/fonts.py +15 -0
  90. starplot/styles/helpers.py +93 -0
  91. starplot/styles/markers.py +308 -0
  92. starplot/utils.py +169 -0
  93. starplot/warnings.py +21 -0
  94. starplot-0.20.5.dist-info/METADATA +146 -0
  95. starplot-0.20.5.dist-info/RECORD +98 -0
  96. starplot-0.20.5.dist-info/WHEEL +4 -0
  97. starplot-0.20.5.dist-info/entry_points.txt +3 -0
  98. starplot-0.20.5.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,342 @@
1
+ import math
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ import pyproj
6
+ from matplotlib import patches
7
+ from pydantic import BaseModel, computed_field
8
+
9
+ from starplot.utils import in_circle
10
+
11
+
12
+ class Optic(BaseModel, ABC):
13
+ """Abstract class for defining Optics."""
14
+
15
+ def __str__(self):
16
+ return "Optic"
17
+
18
+ @property
19
+ @abstractmethod
20
+ def xlim(self):
21
+ pass
22
+
23
+ @property
24
+ @abstractmethod
25
+ def ylim(self):
26
+ pass
27
+
28
+ @property
29
+ @abstractmethod
30
+ def label(self):
31
+ return "Abstract Optic"
32
+
33
+ @abstractmethod
34
+ def patch(self, center_x, center_y) -> patches.Patch:
35
+ pass
36
+
37
+ def transform(self, axis) -> None:
38
+ pass
39
+
40
+ @abstractmethod
41
+ def in_bounds(self, x, y, scale: float = 1) -> bool:
42
+ pass
43
+
44
+ def _compute_radius(self, radius_degrees: float, x: float = 0, y: float = 0):
45
+ geod = pyproj.Geod("+a=6378137 +f=0.0", sphere=True)
46
+ _, _, distance = geod.inv(x, y, x + radius_degrees, y)
47
+ return distance
48
+
49
+
50
+ class Scope(Optic):
51
+ """Creates a new generic Scope optic.
52
+
53
+ Use this class to create custom scope optics or use it as a generic optic that does NOT apply any transforms to the view.
54
+
55
+ See subclasses of this optic for more specific use cases:
56
+
57
+ - [`Refractor`][starplot.models.Refractor] - automatically inverts the view (i.e. assumes a star diagonal is used)
58
+
59
+ - [`Reflector`][starplot.models.Reflector] - automatically rotates the view so it's upside-down
60
+
61
+ Args:
62
+ focal_length: Focal length (mm) of the telescope
63
+ eyepiece_focal_length: Focal length (mm) of the eyepiece
64
+ eyepiece_fov: Field of view (degrees) of the eyepiece
65
+
66
+ Returns:
67
+ Scope: A new instance of a Scope optic
68
+ """
69
+
70
+ focal_length: float
71
+ """Focal length (mm) of the telescope"""
72
+
73
+ eyepiece_focal_length: float
74
+ """Focal length (mm) of the eyepiece"""
75
+
76
+ eyepiece_fov: float
77
+ """Field of view (degrees) of the eyepiece"""
78
+
79
+ @computed_field
80
+ @property
81
+ def magnification(self) -> float:
82
+ """Magnification calculated from the telescope's focal length and eyepiece focal length"""
83
+ return self.focal_length / self.eyepiece_focal_length
84
+
85
+ @computed_field
86
+ @property
87
+ def true_fov(self) -> float:
88
+ """True field of view of telescope"""
89
+ return self.eyepiece_fov / self.magnification
90
+
91
+ @computed_field
92
+ @property
93
+ def radius(self) -> float:
94
+ return self._compute_radius(self.true_fov / 2)
95
+
96
+ def __str__(self):
97
+ return f"{self.focal_length:.0f}mm w/ {self.eyepiece_focal_length:.0f}mm ({self.magnification:.0f}x) @ {self.eyepiece_fov:.0f}\N{DEGREE SIGN} = {self.true_fov:.2f}\N{DEGREE SIGN} TFOV"
98
+
99
+ @property
100
+ def xlim(self):
101
+ return self.radius
102
+
103
+ @property
104
+ def ylim(self):
105
+ return self.radius
106
+
107
+ @property
108
+ def label(self):
109
+ return "Scope"
110
+
111
+ def patch(self, center_x, center_y, **kwargs):
112
+ padding = kwargs.pop("padding", 0)
113
+ return patches.Circle(
114
+ (center_x, center_y),
115
+ radius=self.radius + padding,
116
+ **kwargs,
117
+ )
118
+
119
+ def in_bounds(self, x, y, scale: float = 1) -> bool:
120
+ return in_circle(x, y, 0, 0, self.radius * scale)
121
+
122
+
123
+ class Refractor(Scope):
124
+ """Creates a new Refractor Telescope optic
125
+
126
+ Warning:
127
+ This optic assumes a star diagonal is used, so it applies a transform that inverts the image.
128
+
129
+ If you don't want this transform applied, then use the generic [`Scope`][starplot.models.Scope] optic instead.
130
+
131
+ Args:
132
+ focal_length: Focal length (mm) of the telescope
133
+ eyepiece_focal_length: Focal length (mm) of the eyepiece
134
+ eyepiece_fov: Field of view (degrees) of the eyepiece
135
+
136
+ Returns:
137
+ Refractor: A new instance of a Refractor optic
138
+
139
+ """
140
+
141
+ @property
142
+ def label(self):
143
+ return "Refractor"
144
+
145
+ def transform(self, axis) -> None:
146
+ axis.invert_xaxis()
147
+
148
+
149
+ class Reflector(Scope):
150
+ """Creates a new Reflector Telescope optic
151
+
152
+ Warning:
153
+ This optic applies a transform that produces an "upside-down" image.
154
+
155
+ If you don't want this transform applied, then use the generic [`Scope`][starplot.models.Scope] optic instead.
156
+
157
+ Args:
158
+ focal_length: Focal length (mm) of the telescope
159
+ eyepiece_focal_length: Focal length (mm) of the eyepiece
160
+ eyepiece_fov: Field of view (degrees) of the eyepiece
161
+
162
+ Returns:
163
+ Reflector: A new instance of a Reflector optic
164
+
165
+ """
166
+
167
+ @property
168
+ def label(self):
169
+ return "Reflector"
170
+
171
+ def transform(self, axis) -> None:
172
+ axis.invert_xaxis()
173
+ axis.invert_yaxis()
174
+
175
+
176
+ class Binoculars(Optic):
177
+ """Creates a new Binoculars optic
178
+
179
+ Args:
180
+ magnification: Magnification of the binoculars
181
+ fov: Apparent field of view (FOV) of the binoculars in degrees. This isn't always easy to find for binoculars, so if you can't find it in your binocular's specs, then try using `60`.
182
+
183
+ Returns:
184
+ Binoculars: A new instance of a Binoculars optic
185
+
186
+ """
187
+
188
+ magnification: float
189
+ """Magnification of the binoculars"""
190
+
191
+ fov: float
192
+ """Apparent field of view of the binoculars"""
193
+
194
+ @computed_field
195
+ @property
196
+ def true_fov(self) -> float:
197
+ """True field of view of binoculars"""
198
+ return self.fov / self.magnification
199
+
200
+ @computed_field
201
+ @property
202
+ def radius(self) -> float:
203
+ return self._compute_radius(self.true_fov / 2)
204
+
205
+ def __str__(self):
206
+ return f"{self.magnification:.0f}x @ {self.fov:.0f}\N{DEGREE SIGN} = {self.true_fov}\N{DEGREE SIGN}"
207
+
208
+ @property
209
+ def xlim(self):
210
+ return self.radius
211
+
212
+ @property
213
+ def ylim(self):
214
+ return self.radius
215
+
216
+ @property
217
+ def label(self):
218
+ return "Binoculars"
219
+
220
+ def patch(self, center_x, center_y, **kwargs):
221
+ padding = kwargs.pop("padding", 0)
222
+ return patches.Circle(
223
+ (center_x, center_y),
224
+ radius=self.radius + padding,
225
+ **kwargs,
226
+ )
227
+
228
+ def in_bounds(self, x, y, scale: float = 1) -> bool:
229
+ return in_circle(x, y, 0, 0, self.radius * scale)
230
+
231
+
232
+ class Camera(Optic):
233
+ """Creates a new Camera optic
234
+
235
+ Note:
236
+ Field of view for each dimension is calculated using the following formula:
237
+
238
+ ```
239
+ TFOV = 2 * arctan( d / (2 * f) )
240
+ ```
241
+
242
+ _Where_:
243
+
244
+ d = sensor size (height or width)
245
+
246
+ f = focal length of lens
247
+
248
+ Args:
249
+ sensor_height: Height of camera sensor (mm)
250
+ sensor_width: Width of camera sensor (mm)
251
+ lens_focal_length: Focal length of camera lens (mm)
252
+ rotation: Angle (degrees) to rotate camera, defaults to 0
253
+
254
+ Returns:
255
+ Camera: A new instance of a Camera optic
256
+
257
+ """
258
+
259
+ sensor_height: float
260
+ """Height (mm) of camera's sensor"""
261
+
262
+ sensor_width: float
263
+ """Width (mm) of camera's sensor"""
264
+
265
+ lens_focal_length: float
266
+ """Focal length (mm) of the camera's lens"""
267
+
268
+ rotation: float = 0
269
+ """Angle (degrees) to rotate the camera"""
270
+
271
+ @computed_field
272
+ @property
273
+ def true_fov_x(self) -> float:
274
+ return 2 * math.degrees(
275
+ math.atan(self.sensor_width / (2 * self.lens_focal_length))
276
+ )
277
+
278
+ @computed_field
279
+ @property
280
+ def true_fov_y(self) -> float:
281
+ return 2 * math.degrees(
282
+ math.atan(self.sensor_height / (2 * self.lens_focal_length))
283
+ )
284
+
285
+ @computed_field
286
+ @property
287
+ def true_fov(self) -> float:
288
+ return max(self.true_fov_x, self.true_fov_y)
289
+
290
+ @computed_field
291
+ @property
292
+ def radius_x(self) -> float:
293
+ return self._compute_radius(self.true_fov_x / 2)
294
+
295
+ @computed_field
296
+ @property
297
+ def radius_y(self) -> float:
298
+ return self._compute_radius(self.true_fov_y / 2)
299
+
300
+ def __str__(self):
301
+ return f"{self.sensor_width}x{self.sensor_height} w/ {self.lens_focal_length:.0f}mm lens = {self.true_fov_x:.2f}\N{DEGREE SIGN} x {self.true_fov_y:.2f}\N{DEGREE SIGN}"
302
+
303
+ @property
304
+ def xlim(self):
305
+ x_offset = self.radius_x * self.rotation / 180
306
+ if self.rotation:
307
+ x_offset *= 1.1
308
+ return self.radius_x + x_offset
309
+
310
+ @property
311
+ def ylim(self):
312
+ y_offset = self.radius_y * math.sin(math.radians(self.rotation))
313
+ if self.rotation:
314
+ y_offset *= 1.2
315
+ return self.radius_y + y_offset
316
+
317
+ @property
318
+ def label(self):
319
+ return "Camera"
320
+
321
+ def patch(self, center_x, center_y, **kwargs):
322
+ padding = kwargs.pop("padding", 0)
323
+ x = center_x - self.radius_x - padding
324
+ y = center_y - self.radius_y - padding
325
+ return patches.Rectangle(
326
+ (x, y),
327
+ self.radius_x * 2 + padding,
328
+ self.radius_y * 2 + padding,
329
+ angle=self.rotation,
330
+ rotation_point="center",
331
+ **kwargs,
332
+ )
333
+
334
+ def in_bounds(self, x, y, scale: float = 1) -> bool:
335
+ radians = math.radians(180 - self.rotation)
336
+
337
+ px = x * math.cos(radians) - y * math.sin(radians)
338
+ py = x * math.sin(radians) + y * math.cos(radians)
339
+
340
+ in_bounds_x = px < self.radius_x * scale and px > -1 * self.radius_x * scale
341
+ in_bounds_y = py < self.radius_y * scale and py > -1 * self.radius_y * scale
342
+ return in_bounds_x and in_bounds_y
@@ -0,0 +1,137 @@
1
+ from dataclasses import dataclass
2
+ from datetime import datetime
3
+ from enum import Enum
4
+ from typing import Iterator
5
+
6
+ import numpy as np
7
+
8
+ from shapely import Polygon
9
+ from skyfield.api import Angle
10
+
11
+ from starplot.data import load
12
+ from starplot.models.base import SkyObject
13
+ from starplot.models.observer import Observer
14
+ from starplot.geometry import circle
15
+
16
+
17
+ class PlanetName(str, Enum):
18
+ """Planet names"""
19
+
20
+ MERCURY = "mercury"
21
+ VENUS = "venus"
22
+ MARS = "mars"
23
+ JUPITER = "jupiter"
24
+ SATURN = "saturn"
25
+ URANUS = "uranus"
26
+ NEPTUNE = "neptune"
27
+ PLUTO = "pluto"
28
+
29
+
30
+ PLANET_LABELS_DEFAULT = {p: p.value.upper() for p in PlanetName}
31
+
32
+ PLANET_RADIUS_KM = {
33
+ PlanetName.MERCURY: 2_440,
34
+ PlanetName.VENUS: 6_052,
35
+ PlanetName.MARS: 3_390,
36
+ PlanetName.JUPITER: 69_911,
37
+ PlanetName.SATURN: 58_232,
38
+ PlanetName.URANUS: 25_362,
39
+ PlanetName.NEPTUNE: 24_622,
40
+ PlanetName.PLUTO: 1_151,
41
+ }
42
+ """
43
+ Planet radii in kilometers, via NASA:
44
+
45
+ - https://science.nasa.gov/resource/solar-system-sizes/
46
+ - https://science.nasa.gov/dwarf-planets/pluto/facts/
47
+
48
+ Retrieved on 18-APR-2024
49
+ """
50
+
51
+
52
+ @dataclass(slots=True, kw_only=True)
53
+ class Planet(SkyObject):
54
+ """Planet model."""
55
+
56
+ name: str = None
57
+ """
58
+ Name of the planet:
59
+
60
+ - Mercury
61
+ - Venus
62
+ - Mars
63
+ - Jupiter
64
+ - Saturn
65
+ - Uranus
66
+ - Neptune
67
+ - Pluto
68
+
69
+ """
70
+
71
+ dt: datetime = None
72
+ """Date/time of planet's position"""
73
+
74
+ apparent_size: float = 0
75
+ """Apparent diameter (degrees)"""
76
+
77
+ geometry: Polygon = None
78
+ """Shapely Polygon of the planet's extent. Right ascension coordinates are in degrees (0...360)."""
79
+
80
+ @classmethod
81
+ def all(
82
+ cls,
83
+ observer: Observer = None,
84
+ ephemeris: str = "de421.bsp",
85
+ ) -> Iterator["Planet"]:
86
+ """
87
+ Iterator for getting all planets at a specific date/time and observing location.
88
+
89
+ Args:
90
+ observer: Observer instance that specifies a time and location
91
+ ephemeris: Ephemeris to use for calculating planet positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
92
+ """
93
+
94
+ observer = observer or Observer(lat=None, lon=None)
95
+ eph = load(ephemeris)
96
+
97
+ for p in PlanetName:
98
+ planet = eph[f"{p.value} barycenter"]
99
+ ra, dec, distance = observer._astrometric(planet, ephemeris=ephemeris)
100
+
101
+ # angular diameter:
102
+ # https://rhodesmill.org/skyfield/examples.html#what-is-the-angular-diameter-of-a-planet-given-its-radius
103
+ apparent_diameter_degrees = Angle(
104
+ radians=np.arcsin(PLANET_RADIUS_KM[p] / distance.km) * 2.0
105
+ ).degrees
106
+
107
+ yield Planet(
108
+ ra=ra.hours * 15,
109
+ dec=dec.degrees,
110
+ name=p,
111
+ dt=observer.dt,
112
+ apparent_size=apparent_diameter_degrees,
113
+ geometry=circle(
114
+ (ra.hours * 15, dec.degrees), apparent_diameter_degrees
115
+ ),
116
+ )
117
+
118
+ @classmethod
119
+ def get(
120
+ cls,
121
+ name: str,
122
+ observer: Observer = None,
123
+ ephemeris: str = "de421.bsp",
124
+ ) -> "Planet":
125
+ """
126
+ Get a planet for a specific date/time.
127
+
128
+ Args:
129
+ name: Name of the planet you want to get (see [`Planet.name`][starplot.Planet.name] for options). Case insensitive.
130
+ observer: Observer instance that specifies a time and location
131
+ ephemeris: Ephemeris to use for calculating planet positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
132
+ """
133
+ for p in cls.all(observer, ephemeris):
134
+ if p.name.lower() == name.lower():
135
+ return p
136
+
137
+ return None
@@ -0,0 +1,138 @@
1
+ from dataclasses import dataclass
2
+ from datetime import datetime, timedelta
3
+ from typing import Iterator
4
+
5
+ from shapely import Point
6
+ from skyfield.api import wgs84, EarthSatellite
7
+
8
+ from starplot.data import load
9
+ from starplot.models.base import SkyObject
10
+ from starplot.models.observer import Observer
11
+
12
+ ts = load.timescale()
13
+
14
+
15
+ @dataclass(slots=True, kw_only=True)
16
+ class Satellite(SkyObject):
17
+ """
18
+ Satellites can be created in two ways:
19
+
20
+ 1. [`from_tle`][starplot.Satellite.from_tle] (two-line element set)
21
+ 2. [`from_json`][starplot.Satellite.from_json] (CelesTrak JSON)
22
+
23
+ """
24
+
25
+ name: str = None
26
+ """Name of the satellite"""
27
+
28
+ observer: Observer = None
29
+ """Observer of this satellite instance"""
30
+
31
+ distance: float | None = None
32
+ """Distance to satellite, in Astronomical units (the Earth-Sun distance of 149,597,870,700 m)"""
33
+
34
+ geometry: Point = None
35
+ """Shapely Point of the satellite's position. Right ascension coordinates are in degrees (0...360)."""
36
+
37
+ _satellite: EarthSatellite = None
38
+
39
+ @classmethod
40
+ def from_json(
41
+ cls,
42
+ data: dict,
43
+ observer: Observer = None,
44
+ ) -> "Satellite":
45
+ """
46
+ Get a satellite for a specific date/time/location from a CelesTrak JSON.
47
+
48
+ Args:
49
+ data: Dictionary of the CelesTrak JSON
50
+ observer: Observer instance that specifies a time and location
51
+ """
52
+ observer = observer or Observer()
53
+ return get_satellite_at_date_location(
54
+ satellite=EarthSatellite.from_omm(ts, data),
55
+ observer=observer,
56
+ )
57
+
58
+ @classmethod
59
+ def from_tle(
60
+ cls,
61
+ name: str,
62
+ line1: str,
63
+ line2: str,
64
+ observer: Observer = None,
65
+ ) -> "Satellite":
66
+ """
67
+ Get a satellite for a specific date/time/location from a two-line element set (TLE).
68
+
69
+ Args:
70
+ name: Name of the satellite
71
+ line1: Line 1 of the two-line element set (TLE)
72
+ line2: Line 2 of the two-line element set (TLE)
73
+ observer: Observer instance that specifies a time and location
74
+
75
+ """
76
+ observer = observer or Observer()
77
+ return get_satellite_at_date_location(
78
+ satellite=EarthSatellite(
79
+ line1,
80
+ line2,
81
+ name,
82
+ ts,
83
+ ),
84
+ observer=observer,
85
+ )
86
+
87
+ def trajectory(
88
+ self, date_start: datetime, date_end: datetime, step: timedelta = None
89
+ ) -> Iterator["Satellite"]:
90
+ """
91
+ Iterator for getting a trajectory of the satellite.
92
+
93
+ Args:
94
+ date_start: Starting date/time for the trajectory (inclusive)
95
+ date_end: End date/time for the trajectory (exclusive)
96
+ step: Time-step for the trajectory. Defaults to 1-day
97
+
98
+ Returns:
99
+ Iterator that yields a Satellite instance at each step in the date range
100
+ """
101
+ dt = date_start
102
+ step = step or timedelta(hours=1)
103
+
104
+ while dt < date_end:
105
+ observer_kwargs = self.observer.model_dump()
106
+ observer_kwargs["dt"] = dt
107
+ new_observer = Observer(**observer_kwargs)
108
+ yield get_satellite_at_date_location(
109
+ satellite=self._satellite, observer=new_observer
110
+ )
111
+ dt += step
112
+
113
+
114
+ def get_satellite_at_date_location(
115
+ satellite: EarthSatellite, observer: Observer
116
+ ) -> Satellite:
117
+ t = observer.timescale
118
+ lat = observer.lat
119
+ lon = observer.lon
120
+
121
+ if lat is not None and lon is not None:
122
+ position = wgs84.latlon(lat, lon, observer.elevation)
123
+ difference = satellite - position
124
+ topocentric = difference.at(t)
125
+ ra, dec, distance = topocentric.radec()
126
+ else:
127
+ ra, dec, distance = satellite.at(t).radec()
128
+
129
+ result = Satellite(
130
+ name=satellite.name,
131
+ ra=ra.hours * 15,
132
+ dec=dec.degrees,
133
+ observer=observer,
134
+ distance=distance.au,
135
+ geometry=Point(ra.hours * 15, dec.degrees),
136
+ )
137
+ setattr(result, "_satellite", satellite)
138
+ return result