starplot 0.16.4__py2.py3-none-any.whl → 0.17.0__py2.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 (37) hide show
  1. starplot/__init__.py +28 -2
  2. starplot/base.py +36 -56
  3. starplot/config.py +16 -0
  4. starplot/data/constellations.py +5 -676
  5. starplot/data/dsos.py +7 -23
  6. starplot/data/library/bigsky.0.4.0.stars.mag11.parquet +0 -0
  7. starplot/data/library/sky.db +0 -0
  8. starplot/data/stars.py +12 -2
  9. starplot/data/translations.py +161 -0
  10. starplot/geometry.py +7 -6
  11. starplot/horizon.py +9 -8
  12. starplot/map.py +1 -1
  13. starplot/models/__init__.py +19 -7
  14. starplot/models/base.py +1 -1
  15. starplot/models/comet.py +332 -0
  16. starplot/models/constellation.py +10 -0
  17. starplot/models/dso.py +24 -0
  18. starplot/{optics.py → models/optics.py} +4 -4
  19. starplot/models/satellite.py +158 -0
  20. starplot/models/star.py +10 -0
  21. starplot/optic.py +10 -9
  22. starplot/plotters/__init__.py +1 -0
  23. starplot/plotters/arrow.py +162 -0
  24. starplot/plotters/constellations.py +15 -51
  25. starplot/plotters/dsos.py +8 -14
  26. starplot/plotters/experimental.py +559 -6
  27. starplot/plotters/legend.py +5 -0
  28. starplot/plotters/stars.py +7 -16
  29. starplot/styles/base.py +20 -1
  30. starplot/styles/extensions.py +10 -1
  31. starplot/zenith.py +4 -1
  32. {starplot-0.16.4.dist-info → starplot-0.17.0.dist-info}/METADATA +19 -15
  33. {starplot-0.16.4.dist-info → starplot-0.17.0.dist-info}/RECORD +37 -33
  34. /starplot/{observer.py → models/observer.py} +0 -0
  35. {starplot-0.16.4.dist-info → starplot-0.17.0.dist-info}/WHEEL +0 -0
  36. {starplot-0.16.4.dist-info → starplot-0.17.0.dist-info}/entry_points.txt +0 -0
  37. {starplot-0.16.4.dist-info → starplot-0.17.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,332 @@
1
+ from datetime import datetime, timedelta
2
+ from typing import Iterator
3
+ from functools import cache
4
+ from dataclasses import dataclass, fields
5
+
6
+ from skyfield.api import wgs84
7
+ from skyfield.data import mpc
8
+ from skyfield.constants import GM_SUN_Pitjeva_2005_km3_s2 as GM_SUN
9
+
10
+ from starplot.data import load
11
+ from starplot.models.base import SkyObject, ShapelyPoint
12
+ from starplot.utils import dt_or_now
13
+
14
+
15
+ @dataclass
16
+ class SkyfieldComet:
17
+ designation: str
18
+ reference: str
19
+
20
+ perihelion_year: int
21
+ perihelion_month: int
22
+ perihelion_day: float
23
+ perihelion_distance_au: float
24
+ eccentricity: float
25
+ argument_of_perihelion_degrees: float
26
+ longitude_of_ascending_node_degrees: float
27
+ inclination_degrees: float
28
+
29
+ perturbed_epoch_year: int | None = None
30
+ perturbed_epoch_month: int | None = None
31
+ perturbed_epoch_day: int | None = None
32
+
33
+ number: int | None = None
34
+ designation_packed: str | None = None
35
+ orbit_type: str | None = None
36
+ magnitude_g: float | None = None
37
+ magnitude_k: float | None = None
38
+
39
+ # Skyfield columns
40
+ # ('number', (0, 4)),
41
+ # ('orbit_type', (4, 5)),
42
+ # ('designation_packed', (5, 12)),
43
+ # ('perihelion_year', (14, 18)),
44
+ # ('perihelion_month', (19, 21)),
45
+ # ('perihelion_day', (22, 29)),
46
+ # ('perihelion_distance_au', (30, 39)),
47
+ # ('eccentricity', (41, 49)),
48
+ # ('argument_of_perihelion_degrees', (51, 59)),
49
+ # ('longitude_of_ascending_node_degrees', (61, 69)),
50
+ # ('inclination_degrees', (71, 79)),
51
+ # ('perturbed_epoch_year', (81, 85)),
52
+ # ('perturbed_epoch_month', (85, 87)),
53
+ # ('perturbed_epoch_day', (87, 89)),
54
+ # ('magnitude_g', (91, 95)),
55
+ # ('magnitude_k', (96, 100)),
56
+ # ('designation', (102, 158)),
57
+ # ('reference', (159, 168)),
58
+
59
+ """
60
+ MPC JSON
61
+ {
62
+ "Comet_num": 483,
63
+ "Orbit_type": "P",
64
+ "Year_of_perihelion": 2027,
65
+ "Month_of_perihelion": 11,
66
+ "Day_of_perihelion": 11.7324,
67
+ "Perihelion_dist": 2.486784,
68
+ "e": 0.221733,
69
+ "Peri": 49.461,
70
+ "Node": 199.0602,
71
+ "i": 14.1756,
72
+ "Epoch_year": 2025,
73
+ "Epoch_month": 10,
74
+ "Epoch_day": 22,
75
+ "H": 17.0,
76
+ "G": 4.0,
77
+ "Designation_and_name": "483P-B/PANSTARRS",
78
+ "Ref": "MPC185666"
79
+ }
80
+ """
81
+
82
+ @classmethod
83
+ def from_mpc_json(cls, data: dict) -> "SkyfieldComet":
84
+ """Converts an MPC orbit JSON for a comet to a dataclass that Skyfield can work with"""
85
+
86
+ d = {k.lower(): v for k, v in data.items()}
87
+
88
+ return SkyfieldComet(
89
+ number=d.get("comet_num"),
90
+ orbit_type=d.get("orbit_type"),
91
+ designation_packed=d.get("provisional_packed_desig"),
92
+ perihelion_year=d.get("year_of_perihelion"),
93
+ perihelion_month=d.get("month_of_perihelion"),
94
+ perihelion_day=d.get("day_of_perihelion"),
95
+ perihelion_distance_au=d.get("perihelion_dist"),
96
+ eccentricity=d.get("e"),
97
+ argument_of_perihelion_degrees=d.get("peri"),
98
+ longitude_of_ascending_node_degrees=d.get("node"),
99
+ inclination_degrees=d.get("i"),
100
+ perturbed_epoch_year=d.get("epoch_year"),
101
+ perturbed_epoch_month=d.get("epoch_month"),
102
+ perturbed_epoch_day=d.get("epoch_day"),
103
+ magnitude_g=d.get("g"),
104
+ # magnitude_k=None, # no mapping?
105
+ designation=d.get("designation_and_name"),
106
+ reference=d.get("ref"),
107
+ )
108
+
109
+ @classmethod
110
+ def from_mpc_remote(cls, name: str, reload: bool) -> "SkyfieldComet":
111
+ """
112
+ Create a Skyfield comet by downloading the latest MPC comet data and finding the comet by name/designation
113
+
114
+ Args:
115
+ name: Name/designation of the comet
116
+ reload: If True, then the MPC data will be re-downloaded before finding the comet
117
+ """
118
+ comets = get_comets(reload)
119
+ row = comets.loc[name]
120
+ return SkyfieldComet(**row.to_dict())
121
+
122
+ def __getitem__(self, key):
123
+ """Allows accessing dataclass fields using dictionary-like syntax."""
124
+ if key in [f.name for f in fields(self)]:
125
+ return getattr(self, key)
126
+ else:
127
+ raise KeyError
128
+
129
+
130
+ class Comet(SkyObject):
131
+ """
132
+ Comets can be created in three ways:
133
+
134
+ 1. [`get`][starplot.Comet.get] (designation/name)
135
+ 2. [`all`][starplot.Comet.all] (iterate through all comets available from MPC)
136
+ 3. [`from_json`][starplot.Comet.from_json] (IAU MPC JSON)
137
+
138
+ """
139
+
140
+ name: str
141
+ """
142
+ Name of the comet (as designated by IAU Minor Planet Center)
143
+ """
144
+
145
+ dt: datetime
146
+ """Date/time of comet's position"""
147
+
148
+ lat: float | None = None
149
+ """Latitude of observing location"""
150
+
151
+ lon: float | None = None
152
+ """Longitude of observing location"""
153
+
154
+ distance: float | None = None
155
+ """Distance to comet, in Astronomical units (the Earth-Sun distance of 149,597,870,700 m)"""
156
+
157
+ ephemeris: str = None
158
+ """Ephemeris used when retrieving this instance"""
159
+
160
+ geometry: ShapelyPoint = None
161
+ """Shapely Point of the comet's position. Right ascension coordinates are in degrees (0...360)."""
162
+
163
+ data: SkyfieldComet = None
164
+
165
+ @classmethod
166
+ def from_json(
167
+ cls,
168
+ data: dict,
169
+ dt: datetime = None,
170
+ lat: float = None,
171
+ lon: float = None,
172
+ ephemeris: str = "de421_2001.bsp",
173
+ ) -> "Comet":
174
+ """
175
+ Get a comet for a specific date/time/location from an IAU MPC JSON.
176
+
177
+ Args:
178
+ data: Dictionary of the IAU MPC JSON
179
+ dt: Datetime you want the comet for (must be timezone aware!). _Defaults to current UTC time_.
180
+ lat: Latitude of observing location. If you set this (and longitude), then the comet's _apparent_ RA/DEC will be calculated.
181
+ lon: Longitude of observing location
182
+ ephemeris: Ephemeris to use for calculating comet positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
183
+ """
184
+ dt = dt_or_now(dt)
185
+ comet = SkyfieldComet.from_mpc_json(data)
186
+
187
+ return get_comet_at_date_location(comet, dt, lat, lon, ephemeris)
188
+
189
+ @classmethod
190
+ def all(
191
+ cls,
192
+ dt: datetime = None,
193
+ lat: float = None,
194
+ lon: float = None,
195
+ ephemeris: str = "de421_2001.bsp",
196
+ reload: bool = False,
197
+ ) -> Iterator["Comet"]:
198
+ """
199
+ Iterator for getting all comets at a specific date/time and observing location.
200
+
201
+ Args:
202
+ dt: Datetime you want the comets for (must be timezone aware!). _Defaults to current UTC time_.
203
+ lat: Latitude of observing location. If you set this (and longitude), then the comet's _apparent_ RA/DEC will be calculated.
204
+ lon: Longitude of observing location
205
+ ephemeris: Ephemeris to use for calculating comet positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
206
+ reload: If True, then the comet data file will be re-downloaded. Otherwise, it'll use the existing file if available.
207
+ """
208
+ dt = dt_or_now(dt)
209
+ comets = get_comets(reload=reload)
210
+ for name in comets.index.values:
211
+ row = comets.loc[name]
212
+ comet = SkyfieldComet(**row.to_dict())
213
+ yield get_comet_at_date_location(
214
+ comet=comet,
215
+ dt=dt,
216
+ lat=lat,
217
+ lon=lon,
218
+ ephemeris=ephemeris,
219
+ )
220
+
221
+ @classmethod
222
+ def get(
223
+ cls,
224
+ name: str,
225
+ dt: datetime = None,
226
+ lat: float = None,
227
+ lon: float = None,
228
+ ephemeris: str = "de421_2001.bsp",
229
+ reload: bool = False,
230
+ ) -> "Comet":
231
+ """
232
+ Get a comet for a specific date/time.
233
+
234
+ Args:
235
+ name: Name of the comet you want to get (as designated by IAU Minor Planet Center)
236
+ dt: Datetime you want the comet for (must be timezone aware!). _Defaults to current UTC time_.
237
+ lat: Latitude of observing location. If you set this (and longitude), then the comet's _apparent_ RA/DEC will be calculated.
238
+ lon: Longitude of observing location
239
+ ephemeris: Ephemeris to use for calculating comet positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
240
+ reload: If True, then the comet data file will be re-downloaded. Otherwise, it'll use the existing file if available.
241
+ """
242
+ dt = dt_or_now(dt)
243
+ comet = SkyfieldComet.from_mpc_remote(name, reload)
244
+ return get_comet_at_date_location(comet, dt, lat, lon, ephemeris)
245
+
246
+ def trajectory(
247
+ self, date_start: datetime, date_end: datetime, step: timedelta = None
248
+ ) -> Iterator["Comet"]:
249
+ """
250
+ Iterator for getting a trajectory of the comet.
251
+
252
+ Args:
253
+ date_start: Starting date/time for the trajectory (inclusive)
254
+ date_end: End date/time for the trajectory (exclusive)
255
+ step: Time-step for the trajectory. Defaults to 1-day
256
+
257
+ Returns:
258
+ Iterator that yields a Comet instance at each step in the date range
259
+ """
260
+
261
+ step = step or timedelta(days=1)
262
+ dt = date_start
263
+
264
+ while dt < date_end:
265
+ yield get_comet_at_date_location(
266
+ comet=self.data,
267
+ dt=dt,
268
+ lat=self.lat,
269
+ lon=self.lon,
270
+ ephemeris=self.ephemeris,
271
+ )
272
+ dt += step
273
+
274
+
275
+ @cache
276
+ def get_comets(reload=False):
277
+ """
278
+ Gets ALL comets currently tracked by IAU Minor Planet Center.
279
+
280
+ Args:
281
+ reload: If True, then redownload the comet data if it already exists
282
+
283
+ Returns:
284
+ DataFrame of all comets, indexed by name
285
+ """
286
+
287
+ with load.open(mpc.COMET_URL, reload=reload) as f:
288
+ comets = mpc.load_comets_dataframe(f)
289
+
290
+ # Keep only the most recent orbit for each comet, and index by designation for fast lookup.
291
+ comets = (
292
+ comets.sort_values("reference")
293
+ .groupby("designation", as_index=False)
294
+ .last()
295
+ .set_index("designation", drop=False)
296
+ )
297
+
298
+ return comets
299
+
300
+
301
+ def get_comet_at_date_location(
302
+ comet: SkyfieldComet, dt: datetime, lat: float, lon: float, ephemeris: str
303
+ ) -> Comet:
304
+ """
305
+ Creates a Comet instance for date and (optional) observing location.
306
+ """
307
+ ts = load.timescale()
308
+ eph = load(ephemeris)
309
+ sun, earth = eph["sun"], eph["earth"]
310
+ c = sun + mpc.comet_orbit(comet, ts, GM_SUN)
311
+ t = ts.from_datetime(dt)
312
+
313
+ if lat is not None and lon is not None:
314
+ position = earth + wgs84.latlon(lat, lon)
315
+ astrometric = position.at(t).observe(c)
316
+ apparent = astrometric.apparent()
317
+ ra, dec, distance = apparent.radec()
318
+ else:
319
+ ra, dec, distance = earth.at(t).observe(c).radec()
320
+
321
+ return Comet(
322
+ name=comet.designation,
323
+ ra=ra.hours * 15,
324
+ dec=dec.degrees,
325
+ dt=dt,
326
+ lat=lat,
327
+ lon=lon,
328
+ distance=distance.au,
329
+ ephemeris=ephemeris,
330
+ geometry=ShapelyPoint(ra.hours * 15, dec.degrees),
331
+ data=comet,
332
+ )
@@ -92,6 +92,16 @@ class Constellation(SkyObject):
92
92
  """Not applicable to Constellation model, raises `NotImplementedError`"""
93
93
  raise NotImplementedError()
94
94
 
95
+ @classmethod
96
+ def get_label(cls, constellation):
97
+ """
98
+ Default function for determining the plotted label for a constellation
99
+
100
+ Returns the uppercase name of the constellation.
101
+
102
+ """
103
+ return constellation.name.upper()
104
+
95
105
 
96
106
  def from_tuple(c: tuple) -> Constellation:
97
107
  c = Constellation(
starplot/models/dso.py CHANGED
@@ -189,6 +189,30 @@ class DSO(SkyObject, CreateMapMixin, CreateOpticMixin):
189
189
  df = load(filters=where, sql=sql).to_pandas()
190
190
  return [from_tuple(d) for d in df.itertuples()]
191
191
 
192
+ @classmethod
193
+ def get_label(cls, dso) -> str:
194
+ """
195
+ Default function for determining the plotted label for a DSO.
196
+
197
+ Returns:
198
+
199
+ 1. `"M13"` if DSO is a Messier object
200
+ 2. `"6456"` if DSO is an NGC object
201
+ 3. `"IC1920"` if DSO is an IC object
202
+ 4. Empty string otherwise
203
+
204
+ """
205
+ if dso.m:
206
+ return f"M{dso.m}"
207
+
208
+ if dso.ngc:
209
+ return f"{dso.ngc}"
210
+
211
+ if dso.ic:
212
+ return f"IC{dso.ic}"
213
+
214
+ return ""
215
+
192
216
 
193
217
  def from_tuple(d: tuple) -> DSO:
194
218
  dso = DSO(
@@ -54,9 +54,9 @@ class Scope(Optic):
54
54
 
55
55
  See subclasses of this optic for more specific use cases:
56
56
 
57
- - [`Refractor`][starplot.optics.Refractor] - automatically inverts the view (i.e. assumes a star diagonal is used)
57
+ - [`Refractor`][starplot.models.Refractor] - automatically inverts the view (i.e. assumes a star diagonal is used)
58
58
 
59
- - [`Reflector`][starplot.optics.Reflector] - automatically rotates the view so it's upside-down
59
+ - [`Reflector`][starplot.models.Reflector] - automatically rotates the view so it's upside-down
60
60
 
61
61
  Args:
62
62
  focal_length: Focal length (mm) of the telescope
@@ -126,7 +126,7 @@ class Refractor(Scope):
126
126
  Warning:
127
127
  This optic assumes a star diagonal is used, so it applies a transform that inverts the image.
128
128
 
129
- If you don't want this transform applied, then use the generic [`Scope`][starplot.optics.Scope] optic instead.
129
+ If you don't want this transform applied, then use the generic [`Scope`][starplot.models.Scope] optic instead.
130
130
 
131
131
  Args:
132
132
  focal_length: Focal length (mm) of the telescope
@@ -152,7 +152,7 @@ class Reflector(Scope):
152
152
  Warning:
153
153
  This optic applies a transform that produces an "upside-down" image.
154
154
 
155
- If you don't want this transform applied, then use the generic [`Scope`][starplot.optics.Scope] optic instead.
155
+ If you don't want this transform applied, then use the generic [`Scope`][starplot.models.Scope] optic instead.
156
156
 
157
157
  Args:
158
158
  focal_length: Focal length (mm) of the telescope
@@ -0,0 +1,158 @@
1
+ from datetime import datetime, timedelta
2
+ from typing import Iterator
3
+
4
+ from skyfield.api import wgs84, EarthSatellite
5
+ from skyfield.timelib import Timescale
6
+
7
+ from starplot.data import load
8
+ from starplot.models.base import SkyObject, ShapelyPoint
9
+ from starplot.utils import dt_or_now
10
+
11
+
12
+ class Satellite(SkyObject):
13
+ """
14
+ Satellites can be created in two ways:
15
+
16
+ 1. [`from_tle`][starplot.Satellite.from_tle] (two-line element set)
17
+ 2. [`from_json`][starplot.Satellite.from_json] (CelesTrak JSON)
18
+
19
+ """
20
+
21
+ name: str
22
+ """
23
+ Name of the satellite
24
+ """
25
+
26
+ dt: datetime
27
+ """Date/time of satellite's position"""
28
+
29
+ lat: float | None = None
30
+ """Latitude of observing location"""
31
+
32
+ lon: float | None = None
33
+ """Longitude of observing location"""
34
+
35
+ distance: float | None = None
36
+ """Distance to satellite, in Astronomical units (the Earth-Sun distance of 149,597,870,700 m)"""
37
+
38
+ ephemeris: str = None
39
+ """Ephemeris used when retrieving this instance"""
40
+
41
+ geometry: ShapelyPoint = None
42
+ """Shapely Point of the satellite's position. Right ascension coordinates are in degrees (0...360)."""
43
+
44
+ _satellite: EarthSatellite
45
+
46
+ @classmethod
47
+ def from_json(
48
+ cls,
49
+ data: dict,
50
+ dt: datetime = None,
51
+ lat: float = None,
52
+ lon: float = None,
53
+ ephemeris: str = "de421_2001.bsp",
54
+ ) -> "Satellite":
55
+ """
56
+ Get a satellite for a specific date/time/location from a CelesTrak JSON.
57
+
58
+ Args:
59
+ data: Dictionary of the CelesTrak JSON
60
+ dt: Datetime you want the satellite for (must be timezone aware!). _Defaults to current UTC time_.
61
+ lat: Latitude of observing location. If you set this (and longitude), then the satellite's _apparent_ RA/DEC will be calculated.
62
+ lon: Longitude of observing location
63
+ ephemeris: Ephemeris to use for calculating satellite positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
64
+ """
65
+ dt = dt_or_now(dt)
66
+ ts = load.timescale()
67
+ satellite = EarthSatellite.from_omm(ts, data)
68
+
69
+ return get_satellite_at_date_location(satellite, dt, lat, lon, ts)
70
+
71
+ @classmethod
72
+ def from_tle(
73
+ cls,
74
+ name: str,
75
+ line1: str,
76
+ line2: str,
77
+ dt: datetime = None,
78
+ lat: float = None,
79
+ lon: float = None,
80
+ ephemeris: str = "de421_2001.bsp",
81
+ ) -> "Satellite":
82
+ """
83
+ Get a satellite for a specific date/time/location from a two-line element set (TLE).
84
+
85
+ Args:
86
+ name: Name of the satellite
87
+ line1: Line 1 of the two-line element set (TLE)
88
+ line2: Line 2 of the two-line element set (TLE)
89
+ dt: Datetime you want the satellite for (must be timezone aware!). _Defaults to current UTC time_.
90
+ lat: Latitude of observing location. If you set this (and longitude), then the satellite's _apparent_ RA/DEC will be calculated.
91
+ lon: Longitude of observing location
92
+ ephemeris: Ephemeris to use for calculating satellite positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
93
+ """
94
+ dt = dt_or_now(dt)
95
+ ts = load.timescale()
96
+ satellite = EarthSatellite(
97
+ line1,
98
+ line2,
99
+ name,
100
+ ts,
101
+ )
102
+
103
+ return get_satellite_at_date_location(satellite, dt, lat, lon, ts)
104
+
105
+ def trajectory(
106
+ self, date_start: datetime, date_end: datetime, step: timedelta = None
107
+ ) -> Iterator["Satellite"]:
108
+ """
109
+ Iterator for getting a trajectory of the satellite.
110
+
111
+ Args:
112
+ date_start: Starting date/time for the trajectory (inclusive)
113
+ date_end: End date/time for the trajectory (exclusive)
114
+ step: Time-step for the trajectory. Defaults to 1-day
115
+
116
+ Returns:
117
+ Iterator that yields a Satellite instance at each step in the date range
118
+ """
119
+
120
+ step = step or timedelta(hours=1)
121
+ dt = date_start
122
+ ts = load.timescale()
123
+
124
+ while dt < date_end:
125
+ yield get_satellite_at_date_location(
126
+ self._satellite, dt, self.lat, self.lon, ts
127
+ )
128
+ dt += step
129
+
130
+
131
+ def get_satellite_at_date_location(
132
+ satellite: EarthSatellite, dt: datetime, lat: float, lon: float, ts: Timescale
133
+ ) -> Satellite:
134
+ t = ts.from_datetime(dt)
135
+
136
+ if lat is not None and lon is not None:
137
+ position = wgs84.latlon(lat, lon)
138
+ difference = satellite - position
139
+ topocentric = difference.at(t)
140
+ ra, dec, distance = topocentric.radec()
141
+ # alt, az, distance = topocentric.altaz()
142
+ # print(alt, az)
143
+ else:
144
+ ra, dec, distance = satellite.at(t).radec()
145
+
146
+ result = Satellite(
147
+ name=satellite.name,
148
+ ra=ra.hours * 15,
149
+ dec=dec.degrees,
150
+ dt=dt,
151
+ lat=lat,
152
+ lon=lon,
153
+ distance=distance.au,
154
+ ephemeris="na",
155
+ geometry=ShapelyPoint(ra.hours * 15, dec.degrees),
156
+ )
157
+ setattr(result, "_satellite", satellite)
158
+ return result
starplot/models/star.py CHANGED
@@ -134,6 +134,16 @@ class Star(SkyObject):
134
134
 
135
135
  return [from_tuple(s) for s in df.itertuples()]
136
136
 
137
+ @classmethod
138
+ def get_label(cls, star) -> str:
139
+ """
140
+ Default function for determining the plotted label for a Star.
141
+
142
+ Returns:
143
+ The star's name
144
+ """
145
+ return star.name
146
+
137
147
 
138
148
  def from_tuple(star: tuple) -> Star:
139
149
  s = Star(
starplot/optic.py CHANGED
@@ -1,7 +1,5 @@
1
1
  from typing import Callable, Mapping
2
2
 
3
- # import pandas as pd
4
-
5
3
  from cartopy import crs as ccrs
6
4
  from matplotlib import pyplot as plt, patches, path
7
5
  from skyfield.api import wgs84, Star as SkyfieldStar
@@ -11,10 +9,14 @@ from starplot import callables
11
9
  from starplot.base import BasePlot, DPI
12
10
  from starplot.data.stars import StarCatalog
13
11
  from starplot.mixins import ExtentMaskMixin
14
- from starplot.models import Star
15
- from starplot.observer import Observer
16
- from starplot.optics import Optic, Camera
17
- from starplot.plotters import StarPlotterMixin, DsoPlotterMixin, GradientBackgroundMixin
12
+ from starplot.models import Star, Optic, Camera
13
+ from starplot.models.observer import Observer
14
+ from starplot.plotters import (
15
+ StarPlotterMixin,
16
+ DsoPlotterMixin,
17
+ GradientBackgroundMixin,
18
+ LegendPlotterMixin,
19
+ )
18
20
  from starplot.styles import (
19
21
  PlotStyle,
20
22
  ObjectStyle,
@@ -26,8 +28,6 @@ from starplot.styles import (
26
28
  )
27
29
  from starplot.utils import azimuth_to_string
28
30
 
29
- # pd.options.mode.chained_assignment = None # default='warn'
30
-
31
31
  DEFAULT_OPTIC_STYLE = PlotStyle().extend(extensions.OPTIC)
32
32
 
33
33
 
@@ -37,6 +37,7 @@ class OpticPlot(
37
37
  StarPlotterMixin,
38
38
  DsoPlotterMixin,
39
39
  GradientBackgroundMixin,
40
+ LegendPlotterMixin,
40
41
  ):
41
42
  """Creates a new optic plot.
42
43
 
@@ -234,7 +235,7 @@ class OpticPlot(
234
235
  size_fn: Callable[[Star], float] = callables.size_by_magnitude_for_optic,
235
236
  alpha_fn: Callable[[Star], float] = callables.alpha_by_magnitude,
236
237
  color_fn: Callable[[Star], str] = None,
237
- label_fn: Callable[[Star], str] = None,
238
+ label_fn: Callable[[Star], str] = Star.get_label,
238
239
  labels: Mapping[int, str] = None,
239
240
  legend_label: str = "Star",
240
241
  bayer_labels: bool = False,
@@ -4,3 +4,4 @@ from .dsos import DsoPlotterMixin # noqa: F401
4
4
  from .milkyway import MilkyWayPlotterMixin # noqa: F401
5
5
  from .legend import LegendPlotterMixin # noqa: F401
6
6
  from .gradients import GradientBackgroundMixin # noqa: F401
7
+ from .arrow import ArrowPlotterMixin # noqa: F401