starplot 0.12.4__py2.py3-none-any.whl → 0.13.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.

Potentially problematic release.


This version of starplot might be problematic. Click here for more details.

Files changed (63) hide show
  1. starplot/__init__.py +1 -1
  2. starplot/base.py +371 -60
  3. starplot/callables.py +61 -7
  4. starplot/data/bayer.py +1532 -3
  5. starplot/data/flamsteed.py +2682 -0
  6. starplot/data/library/constellation_borders_inv.gpkg +0 -0
  7. starplot/data/library/constellation_lines_hips.json +3 -1
  8. starplot/data/stars.py +408 -87
  9. starplot/horizon.py +398 -0
  10. starplot/map.py +149 -24
  11. starplot/models/constellation.py +1 -0
  12. starplot/optic.py +26 -14
  13. starplot/plotters/dsos.py +6 -2
  14. starplot/plotters/stars.py +114 -13
  15. starplot/styles/base.py +263 -156
  16. starplot/styles/ext/antique.yml +45 -39
  17. starplot/styles/ext/blue_dark.yml +32 -36
  18. starplot/styles/ext/blue_light.yml +43 -25
  19. starplot/styles/ext/blue_medium.yml +48 -44
  20. starplot/styles/ext/cb_wong.yml +7 -0
  21. starplot/styles/ext/grayscale.yml +13 -7
  22. starplot/styles/ext/grayscale_dark.yml +12 -4
  23. starplot/styles/ext/map.yml +4 -4
  24. starplot/styles/ext/nord.yml +32 -32
  25. starplot/styles/ext/optic.yml +7 -5
  26. starplot/styles/fonts-library/gfs-didot/DESCRIPTION.en_us.html +9 -0
  27. starplot/styles/fonts-library/gfs-didot/GFSDidot-Regular.ttf +0 -0
  28. starplot/styles/fonts-library/gfs-didot/METADATA.pb +16 -0
  29. starplot/styles/fonts-library/gfs-didot/OFL.txt +94 -0
  30. starplot/styles/fonts-library/hind/DESCRIPTION.en_us.html +28 -0
  31. starplot/styles/fonts-library/hind/Hind-Bold.ttf +0 -0
  32. starplot/styles/fonts-library/hind/Hind-Light.ttf +0 -0
  33. starplot/styles/fonts-library/hind/Hind-Medium.ttf +0 -0
  34. starplot/styles/fonts-library/hind/Hind-Regular.ttf +0 -0
  35. starplot/styles/fonts-library/hind/Hind-SemiBold.ttf +0 -0
  36. starplot/styles/fonts-library/hind/METADATA.pb +58 -0
  37. starplot/styles/fonts-library/hind/OFL.txt +93 -0
  38. starplot/styles/fonts-library/inter/Inter-Black.ttf +0 -0
  39. starplot/styles/fonts-library/inter/Inter-BlackItalic.ttf +0 -0
  40. starplot/styles/fonts-library/inter/Inter-Bold.ttf +0 -0
  41. starplot/styles/fonts-library/inter/Inter-BoldItalic.ttf +0 -0
  42. starplot/styles/fonts-library/inter/Inter-ExtraBold.ttf +0 -0
  43. starplot/styles/fonts-library/inter/Inter-ExtraBoldItalic.ttf +0 -0
  44. starplot/styles/fonts-library/inter/Inter-ExtraLight.ttf +0 -0
  45. starplot/styles/fonts-library/inter/Inter-ExtraLightItalic.ttf +0 -0
  46. starplot/styles/fonts-library/inter/Inter-Italic.ttf +0 -0
  47. starplot/styles/fonts-library/inter/Inter-Light.ttf +0 -0
  48. starplot/styles/fonts-library/inter/Inter-LightItalic.ttf +0 -0
  49. starplot/styles/fonts-library/inter/Inter-Medium.ttf +0 -0
  50. starplot/styles/fonts-library/inter/Inter-MediumItalic.ttf +0 -0
  51. starplot/styles/fonts-library/inter/Inter-Regular.ttf +0 -0
  52. starplot/styles/fonts-library/inter/Inter-SemiBold.ttf +0 -0
  53. starplot/styles/fonts-library/inter/Inter-SemiBoldItalic.ttf +0 -0
  54. starplot/styles/fonts-library/inter/Inter-Thin.ttf +0 -0
  55. starplot/styles/fonts-library/inter/Inter-ThinItalic.ttf +0 -0
  56. starplot/styles/fonts-library/inter/LICENSE.txt +92 -0
  57. starplot/styles/fonts.py +15 -0
  58. starplot/styles/markers.py +207 -6
  59. {starplot-0.12.4.dist-info → starplot-0.13.0.dist-info}/METADATA +9 -10
  60. starplot-0.13.0.dist-info/RECORD +101 -0
  61. starplot-0.12.4.dist-info/RECORD +0 -67
  62. {starplot-0.12.4.dist-info → starplot-0.13.0.dist-info}/LICENSE +0 -0
  63. {starplot-0.12.4.dist-info → starplot-0.13.0.dist-info}/WHEEL +0 -0
starplot/horizon.py ADDED
@@ -0,0 +1,398 @@
1
+ from datetime import datetime
2
+ from typing import Callable, Mapping
3
+
4
+ import pandas as pd
5
+
6
+ from cartopy import crs as ccrs
7
+ from matplotlib import pyplot as plt, patches, path
8
+ from skyfield.api import wgs84, Star as SkyfieldStar
9
+
10
+ from starplot import callables
11
+ from starplot.base import BasePlot, DPI
12
+ from starplot.data.stars import StarCatalog, STAR_NAMES
13
+ from starplot.mixins import ExtentMaskMixin
14
+ from starplot.models import Star
15
+ from starplot.plotters import StarPlotterMixin, DsoPlotterMixin
16
+ from starplot.styles import (
17
+ PlotStyle,
18
+ ObjectStyle,
19
+ extensions,
20
+ use_style,
21
+ ZOrderEnum,
22
+ )
23
+
24
+ pd.options.mode.chained_assignment = None # default='warn'
25
+
26
+ DEFAULT_OPTIC_STYLE = PlotStyle().extend(extensions.OPTIC)
27
+
28
+
29
+ class HorizonPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
30
+ """Creates a new horizon plot.
31
+
32
+ Args:
33
+ optic: Optic instance that defines optical parameters
34
+ ra: Right ascension of target center, in hours (0...24)
35
+ dec: Declination of target center, in degrees (-90...90)
36
+ lat: Latitude of observer's location
37
+ lon: Longitude of observer's location
38
+ dt: Date/time of observation (*must be timezone-aware*). Default = current UTC time.
39
+ ephemeris: Ephemeris to use for calculating planet positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
40
+ style: Styling for the plot (colors, sizes, fonts, etc)
41
+ resolution: Size (in pixels) of largest dimension of the map
42
+ hide_colliding_labels: If True, then labels will not be plotted if they collide with another existing label
43
+ raise_on_below_horizon: If True, then a ValueError will be raised if the target is below the horizon at the observing time/location
44
+ scale: Scaling factor that will be applied to all relevant sizes in styles (e.g. font size, marker size, line widths, etc). For example, if you want to make everything 2x bigger, then set scale to 2.
45
+ autoscale: If True, then the scale will be automatically set based on resolution
46
+
47
+ Returns:
48
+ OpticPlot: A new instance of an OpticPlot
49
+
50
+ """
51
+
52
+ FIELD_OF_VIEW_MAX = 9.0
53
+
54
+ def __init__(
55
+ self,
56
+ lat: float,
57
+ lon: float,
58
+ altitude: tuple[float, float] = (0, 60),
59
+ azimuth: tuple[float, float] = (0, 90),
60
+ dt: datetime = None,
61
+ ephemeris: str = "de421_2001.bsp",
62
+ style: PlotStyle = DEFAULT_OPTIC_STYLE,
63
+ resolution: int = 2048,
64
+ hide_colliding_labels: bool = True,
65
+ scale: float = 1.0,
66
+ autoscale: bool = False,
67
+ *args,
68
+ **kwargs,
69
+ ) -> "HorizonPlot":
70
+ super().__init__(
71
+ dt,
72
+ ephemeris,
73
+ style,
74
+ resolution,
75
+ hide_colliding_labels,
76
+ scale=scale,
77
+ autoscale=autoscale,
78
+ *args,
79
+ **kwargs,
80
+ )
81
+ self.logger.debug("Creating HorizonPlot...")
82
+ self.alt = altitude
83
+ self.az = azimuth
84
+ self.center_az = sum(azimuth) / 2
85
+ self.lat = lat
86
+ self.lon = lon
87
+
88
+ self._crs = ccrs.CRS(
89
+ proj4_params=[
90
+ ("proj", "latlong"),
91
+ ("a", "6378137"),
92
+ ],
93
+ globe=ccrs.Globe(ellipse="sphere", flattening=0),
94
+ )
95
+
96
+ self._calc_position()
97
+ self._init_plot()
98
+ self._adjust_radec_minmax()
99
+
100
+ def _prepare_coords(self, ra, dec) -> (float, float):
101
+ """Converts RA/DEC to AZ/ALT"""
102
+ point = SkyfieldStar(ra_hours=ra, dec_degrees=dec)
103
+ position = self.observe(point)
104
+ pos_apparent = position.apparent()
105
+ pos_alt, pos_az, _ = pos_apparent.altaz()
106
+ return pos_az.degrees, pos_alt.degrees
107
+
108
+ def _plot_kwargs(self) -> dict:
109
+ return dict(transform=self._crs)
110
+
111
+ def in_bounds(self, ra, dec) -> bool:
112
+ """Determine if a coordinate is within the bounds of the plot.
113
+
114
+ Args:
115
+ ra: Right ascension, in hours (0...24)
116
+ dec: Declination, in degrees (-90...90)
117
+
118
+ Returns:
119
+ True if the coordinate is in bounds, otherwise False
120
+ """
121
+ az, alt = self._prepare_coords(ra, dec)
122
+ return (
123
+ az < self.az[1]
124
+ and az > self.az[0]
125
+ and alt < self.alt[1]
126
+ and alt > self.alt[0]
127
+ )
128
+
129
+ def in_bounds_altaz(self, alt, az, scale: float = 1) -> bool:
130
+ """Determine if a coordinate is within the bounds of the plot.
131
+
132
+ Args:
133
+ alt: Altitude angle in degrees (0...90)
134
+ az: Azimuth angle in degrees (0...360)
135
+
136
+ Returns:
137
+ True if the coordinate is in bounds, otherwise False
138
+ """
139
+ # x, y = self._proj.transform_point(az, alt, self._crs)
140
+ return (
141
+ az < self.az[1]
142
+ and az > self.az[0]
143
+ and alt < self.alt[1]
144
+ and alt > self.alt[0]
145
+ )
146
+
147
+ def _polygon(self, points, style, **kwargs):
148
+ super()._polygon(points, style, transform=self._crs, **kwargs)
149
+
150
+ def _calc_position(self):
151
+ earth = self.ephemeris["earth"]
152
+
153
+ self.location = earth + wgs84.latlon(self.lat, self.lon)
154
+ self.observe = self.location.at(self.timescale).observe
155
+
156
+ # get radec at center horizon
157
+ center = self.location.at(self.timescale).from_altaz(
158
+ alt_degrees=0, az_degrees=self.center_az
159
+ )
160
+ print(self.center_az)
161
+ print(center.radec())
162
+ locations = [
163
+ self.location.at(self.timescale).from_altaz(
164
+ alt_degrees=self.alt[0], az_degrees=self.az[0]
165
+ ), # lower left
166
+ self.location.at(self.timescale).from_altaz(
167
+ alt_degrees=self.alt[0], az_degrees=self.az[1]
168
+ ), # lower right
169
+ self.location.at(self.timescale).from_altaz(
170
+ alt_degrees=self.alt[1], az_degrees=self.center_az
171
+ ), # top center
172
+ # self.location.at(self.timescale).from_altaz(alt_degrees=self.alt[1], az_degrees=self.az[0]), # upper left
173
+ # self.location.at(self.timescale).from_altaz(alt_degrees=self.alt[1], az_degrees=self.az[1]), # upper right
174
+ ]
175
+
176
+ self.ra_min = None
177
+ self.ra_max = None
178
+ self.dec_max = None
179
+ self.dec_min = None
180
+
181
+ for location in locations:
182
+ ra, dec, _ = location.radec()
183
+ ra = ra.hours
184
+ dec = dec.degrees
185
+ if self.ra_min is None or ra < self.ra_min:
186
+ self.ra_min = ra
187
+
188
+ if self.ra_max is None or ra > self.ra_max:
189
+ self.ra_max = ra
190
+
191
+ if self.dec_min is None or dec < self.dec_min:
192
+ self.dec_min = dec
193
+
194
+ if self.dec_max is None or dec > self.dec_max:
195
+ self.dec_max = dec
196
+
197
+ # self.star = SkyfieldStar(ra_hours=self.ra, dec_degrees=self.dec)
198
+ # self.position = self.observe(self.star)
199
+ # self.pos_apparent = self.position.apparent()
200
+ # self.pos_alt, self.pos_az, _ = self.pos_apparent.altaz()
201
+
202
+ # if self.pos_alt.degrees < 0 and self.raise_on_below_horizon:
203
+ # raise ValueError("Target is below horizon at specified time/location.")
204
+
205
+ def _adjust_radec_minmax(self):
206
+ # self.ra_min = self.ra - self.optic.true_fov / 15 * 1.08
207
+ # self.ra_max = self.ra + self.optic.true_fov / 15 * 1.08
208
+ # self.dec_max = self.dec + self.optic.true_fov / 2 * 1.03
209
+ # self.dec_min = self.dec - self.optic.true_fov / 2 * 1.03
210
+
211
+ if self.dec_max > 70 or self.dec_min < -70:
212
+ # naive method of getting all the stars near the poles
213
+ self.ra_min = 0
214
+ self.ra_max = 24
215
+
216
+ # TODO : below are in ra/dec - need to convert to alt/az
217
+ # adjust declination to match extent
218
+ extent = self.ax.get_extent(crs=ccrs.PlateCarree())
219
+ self.dec_min = extent[2]
220
+ self.dec_max = extent[3]
221
+
222
+ # adjust right ascension to match extent
223
+ if self.ra_max < 24:
224
+ ra_min = (-1 * extent[1]) / 15
225
+ ra_max = (-1 * extent[0]) / 15
226
+
227
+ if ra_min < 0 or ra_max < 0:
228
+ ra_min += 24
229
+ ra_max += 24
230
+
231
+ self.ra_min = ra_min
232
+ self.ra_max = ra_max
233
+
234
+ self.logger.debug(
235
+ f"Extent = RA ({self.ra_min:.2f}, {self.ra_max:.2f}) DEC ({self.dec_min:.2f}, {self.dec_max:.2f})"
236
+ )
237
+
238
+ def _in_bounds_xy(self, x: float, y: float) -> bool:
239
+ return self.in_bounds_altaz(y, x) # alt = y, az = x
240
+
241
+ def _prepare_star_coords(self, df):
242
+ stars_apparent = self.observe(SkyfieldStar.from_dataframe(df)).apparent()
243
+ nearby_stars_alt, nearby_stars_az, _ = stars_apparent.altaz()
244
+ df["x"], df["y"] = (
245
+ nearby_stars_az.degrees,
246
+ nearby_stars_alt.degrees,
247
+ )
248
+ return df
249
+
250
+ def _scatter_stars(self, ras, decs, sizes, alphas, colors, style=None, **kwargs):
251
+ plotted = super()._scatter_stars(
252
+ ras, decs, sizes, alphas, colors, style, **kwargs
253
+ )
254
+
255
+ if type(self._background_clip_path) == patches.Rectangle:
256
+ # convert to generic path to handle possible rotation angle:
257
+ clip_path = path.Path(self._background_clip_path.get_corners())
258
+ plotted.set_clip_path(clip_path, transform=self.ax.transData)
259
+ else:
260
+ plotted.set_clip_path(self._background_clip_path)
261
+
262
+ @use_style(ObjectStyle, "star")
263
+ def stars(
264
+ self,
265
+ mag: float = 6.0,
266
+ catalog: StarCatalog = StarCatalog.HIPPARCOS,
267
+ style: ObjectStyle = None,
268
+ rasterize: bool = False,
269
+ size_fn: Callable[[Star], float] = callables.size_by_magnitude,
270
+ alpha_fn: Callable[[Star], float] = callables.alpha_by_magnitude,
271
+ color_fn: Callable[[Star], str] = None,
272
+ where: list = None,
273
+ where_labels: list = None,
274
+ labels: Mapping[int, str] = STAR_NAMES,
275
+ legend_label: str = "Star",
276
+ bayer_labels: bool = False,
277
+ *args,
278
+ **kwargs,
279
+ ):
280
+ """
281
+ Plots stars
282
+
283
+ Args:
284
+ mag: Limiting magnitude of stars to plot
285
+ catalog: The catalog of stars to use
286
+ style: If `None`, then the plot's style for stars will be used
287
+ rasterize: If True, then the stars will be rasterized when plotted, which can speed up exporting to SVG and reduce the file size but with a loss of image quality
288
+ size_fn: Callable for calculating the marker size of each star. If `None`, then the marker style's size will be used.
289
+ alpha_fn: Callable for calculating the alpha value (aka "opacity") of each star. If `None`, then the marker style's alpha will be used.
290
+ color_fn: Callable for calculating the color of each star. If `None`, then the marker style's color will be used.
291
+ where: A list of expressions that determine which stars to plot. See [Selecting Objects](/reference-selecting-objects/) for details.
292
+ where_labels: A list of expressions that determine which stars are labeled on the plot. See [Selecting Objects](/reference-selecting-objects/) for details.
293
+ labels: A dictionary that maps a star's HIP id to the label that'll be plotted for that star. If you want to hide name labels, then set this arg to `None`.
294
+ legend_label: Label for stars in the legend. If `None`, then they will not be in the legend.
295
+ bayer_labels: If True, then Bayer labels for stars will be plotted. Set this to False if you want to hide Bayer labels.
296
+ """
297
+ # optic_star_multiplier = 0.57 * (self.FIELD_OF_VIEW_MAX / self.optic.true_fov)
298
+
299
+ # def size_fn_mx(st: Star) -> float:
300
+ # return size_fn(st) * optic_star_multiplier
301
+
302
+ super().stars(
303
+ mag=mag,
304
+ catalog=catalog,
305
+ style=style,
306
+ rasterize=rasterize,
307
+ size_fn=size_fn,
308
+ alpha_fn=alpha_fn,
309
+ color_fn=color_fn,
310
+ where=where,
311
+ where_labels=where_labels,
312
+ labels=labels,
313
+ legend_label=legend_label,
314
+ bayer_labels=bayer_labels,
315
+ *args,
316
+ **kwargs,
317
+ )
318
+
319
+ def _plot_border(self):
320
+ # since we're using AzimuthalEquidistant projection, the center will always be (0, 0)
321
+ x = 0
322
+ y = 0
323
+
324
+ # Background of Viewable Area
325
+ self._background_clip_path = self.optic.patch(
326
+ x,
327
+ y,
328
+ facecolor=self.style.background_color.as_hex(),
329
+ linewidth=0,
330
+ fill=True,
331
+ zorder=ZOrderEnum.LAYER_1,
332
+ )
333
+ self.ax.add_patch(self._background_clip_path)
334
+
335
+ # Inner Border
336
+ inner_border = self.optic.patch(
337
+ x,
338
+ y,
339
+ linewidth=2 * self.scale,
340
+ edgecolor=self.style.border_line_color.as_hex(),
341
+ fill=False,
342
+ zorder=ZOrderEnum.LAYER_5 + 100,
343
+ )
344
+ self.ax.add_patch(inner_border)
345
+
346
+ # Outer border
347
+ outer_border = self.optic.patch(
348
+ x,
349
+ y,
350
+ padding=0.05,
351
+ linewidth=20 * self.scale,
352
+ edgecolor=self.style.border_bg_color.as_hex(),
353
+ fill=False,
354
+ zorder=ZOrderEnum.LAYER_5,
355
+ )
356
+ self.ax.add_patch(outer_border)
357
+
358
+ def _fit_to_ax(self) -> None:
359
+ bbox = self.ax.get_window_extent().transformed(
360
+ self.fig.dpi_scale_trans.inverted()
361
+ )
362
+ width, height = bbox.width, bbox.height
363
+ self.fig.set_size_inches(width, height)
364
+
365
+ def _init_plot(self):
366
+ self._proj = ccrs.LambertAzimuthalEqualArea(
367
+ central_longitude=sum(self.az) / 2,
368
+ central_latitude=0,
369
+ )
370
+ self._proj.threshold = 100
371
+ self.fig = plt.figure(
372
+ figsize=(self.figure_size, self.figure_size),
373
+ facecolor=self.style.figure_background_color.as_hex(),
374
+ layout="constrained",
375
+ dpi=DPI,
376
+ )
377
+ self.ax = plt.axes(projection=self._proj)
378
+ self.ax.xaxis.set_visible(False)
379
+ self.ax.yaxis.set_visible(False)
380
+ self.ax.axis("off")
381
+
382
+ bounds = [
383
+ self.az[0],
384
+ self.az[1] * 1.2,
385
+ self.alt[0],
386
+ self.alt[1],
387
+ ]
388
+ print(bounds)
389
+
390
+ self.ax.set_extent(bounds, crs=ccrs.PlateCarree())
391
+ self.ax.gridlines()
392
+
393
+ # self._plot_border()
394
+ self._fit_to_ax()
395
+
396
+ # self.ax.set_xlim(-1.06 * self.optic.xlim, 1.06 * self.optic.xlim)
397
+ # self.ax.set_ylim(-1.06 * self.optic.ylim, 1.06 * self.optic.ylim)
398
+ # self.optic.transform(self.ax)