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.
- starplot/__init__.py +59 -0
- starplot/callables.py +176 -0
- starplot/cli.py +29 -0
- starplot/config.py +80 -0
- starplot/coordinates.py +7 -0
- starplot/data/__init__.py +22 -0
- starplot/data/catalogs.py +356 -0
- starplot/data/constellations.py +71 -0
- starplot/data/db.py +33 -0
- starplot/data/dsos.py +85 -0
- starplot/data/ecliptic.py +201 -0
- starplot/data/library/constellation_names.parquet +0 -0
- starplot/data/library/dso_names.parquet +0 -0
- starplot/data/library/readme.md +1 -0
- starplot/data/library/star_designations.parquet +0 -0
- starplot/data/stars.py +79 -0
- starplot/data/translations.py +408 -0
- starplot/data/utils.py +35 -0
- starplot/geometry.py +319 -0
- starplot/mixins.py +360 -0
- starplot/models/__init__.py +21 -0
- starplot/models/base.py +77 -0
- starplot/models/comet.py +302 -0
- starplot/models/constellation.py +151 -0
- starplot/models/dso.py +317 -0
- starplot/models/milky_way.py +30 -0
- starplot/models/moon.py +130 -0
- starplot/models/objects.py +29 -0
- starplot/models/observer.py +125 -0
- starplot/models/optics.py +342 -0
- starplot/models/planet.py +137 -0
- starplot/models/satellite.py +138 -0
- starplot/models/star.py +253 -0
- starplot/models/sun.py +62 -0
- starplot/plots/__init__.py +7 -0
- starplot/plots/base.py +1023 -0
- starplot/plots/galaxy.py +372 -0
- starplot/plots/horizon.py +543 -0
- starplot/plots/map.py +515 -0
- starplot/plots/optic.py +469 -0
- starplot/plots/zenith.py +217 -0
- starplot/plotters/__init__.py +9 -0
- starplot/plotters/arrow.py +174 -0
- starplot/plotters/constellations.py +298 -0
- starplot/plotters/debug.py +21 -0
- starplot/plotters/dsos.py +294 -0
- starplot/plotters/experimental.py +722 -0
- starplot/plotters/gradients.py +153 -0
- starplot/plotters/legend.py +253 -0
- starplot/plotters/milkyway.py +51 -0
- starplot/plotters/stars.py +319 -0
- starplot/plotters/text.py +802 -0
- starplot/profile.py +16 -0
- starplot/projections.py +184 -0
- starplot/styles/__init__.py +6 -0
- starplot/styles/base.py +1344 -0
- starplot/styles/ext/antique.yml +175 -0
- starplot/styles/ext/blue_dark.yml +163 -0
- starplot/styles/ext/blue_gold.yml +147 -0
- starplot/styles/ext/blue_light.yml +123 -0
- starplot/styles/ext/blue_medium.yml +142 -0
- starplot/styles/ext/blue_night.yml +185 -0
- starplot/styles/ext/cb_wong.yml +124 -0
- starplot/styles/ext/color_print.yml +111 -0
- starplot/styles/ext/gradient_presets.yml +158 -0
- starplot/styles/ext/grayscale.yml +94 -0
- starplot/styles/ext/grayscale_dark.yml +136 -0
- starplot/styles/ext/map.yml +12 -0
- starplot/styles/ext/nord.yml +158 -0
- starplot/styles/ext/optic.yml +20 -0
- starplot/styles/ext/publication.yml +8 -0
- starplot/styles/extensions.py +129 -0
- starplot/styles/fonts-library/gfs-didot/DESCRIPTION.en_us.html +9 -0
- starplot/styles/fonts-library/gfs-didot/GFSDidot-Regular.ttf +0 -0
- starplot/styles/fonts-library/gfs-didot/METADATA.pb +16 -0
- starplot/styles/fonts-library/gfs-didot/OFL.txt +94 -0
- starplot/styles/fonts-library/inter/Inter-Bold.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-BoldItalic.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-ExtraBold.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-ExtraLight.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-ExtraLightItalic.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-Italic.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-Light.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-LightItalic.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-Regular.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-SemiBold.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-SemiBoldItalic.ttf +0 -0
- starplot/styles/fonts-library/inter/LICENSE.txt +92 -0
- starplot/styles/fonts.py +15 -0
- starplot/styles/helpers.py +93 -0
- starplot/styles/markers.py +308 -0
- starplot/utils.py +169 -0
- starplot/warnings.py +21 -0
- starplot-0.20.5.dist-info/METADATA +146 -0
- starplot-0.20.5.dist-info/RECORD +98 -0
- starplot-0.20.5.dist-info/WHEEL +4 -0
- starplot-0.20.5.dist-info/entry_points.txt +3 -0
- starplot-0.20.5.dist-info/licenses/LICENSE +21 -0
starplot/plots/optic.py
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from cartopy import crs as ccrs
|
|
6
|
+
from matplotlib import pyplot as plt, patches, path
|
|
7
|
+
from skyfield.api import Star as SkyfieldStar
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
from starplot import callables, geometry
|
|
11
|
+
from starplot.coordinates import CoordinateSystem
|
|
12
|
+
from starplot.plots.base import BasePlot, DPI
|
|
13
|
+
from starplot.data.catalogs import Catalog, BIG_SKY_MAG11
|
|
14
|
+
from starplot.mixins import ExtentMaskMixin
|
|
15
|
+
from starplot.models import Star, Optic, Camera
|
|
16
|
+
from starplot.models.observer import Observer
|
|
17
|
+
from starplot.plotters import (
|
|
18
|
+
StarPlotterMixin,
|
|
19
|
+
DsoPlotterMixin,
|
|
20
|
+
GradientBackgroundMixin,
|
|
21
|
+
LegendPlotterMixin,
|
|
22
|
+
)
|
|
23
|
+
from starplot.styles import (
|
|
24
|
+
PlotStyle,
|
|
25
|
+
ObjectStyle,
|
|
26
|
+
LabelStyle,
|
|
27
|
+
extensions,
|
|
28
|
+
use_style,
|
|
29
|
+
ZOrderEnum,
|
|
30
|
+
GradientDirection,
|
|
31
|
+
)
|
|
32
|
+
from starplot.utils import azimuth_to_string
|
|
33
|
+
from starplot.plotters.text import CollisionHandler
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class OpticPlot(
|
|
37
|
+
BasePlot,
|
|
38
|
+
ExtentMaskMixin,
|
|
39
|
+
StarPlotterMixin,
|
|
40
|
+
DsoPlotterMixin,
|
|
41
|
+
GradientBackgroundMixin,
|
|
42
|
+
LegendPlotterMixin,
|
|
43
|
+
):
|
|
44
|
+
"""Creates a new optic plot.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
ra: Right ascension of target center, in degrees (0...360)
|
|
48
|
+
dec: Declination of target center, in degrees (-90...90)
|
|
49
|
+
optic: Optic instance that defines optical parameters
|
|
50
|
+
observer: Observer instance which specifies a time and place. Defaults to `Observer()`
|
|
51
|
+
ephemeris: Ephemeris to use for calculating planet positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
|
|
52
|
+
style: Styling for the plot (colors, sizes, fonts, etc). If `None`, it defaults to `PlotStyle()`
|
|
53
|
+
resolution: Size (in pixels) of largest dimension of the map
|
|
54
|
+
point_label_handler: Default [CollisionHandler][starplot.CollisionHandler] for point labels.
|
|
55
|
+
area_label_handler: Default [CollisionHandler][starplot.CollisionHandler] for area labels.
|
|
56
|
+
path_label_handler: Default [CollisionHandler][starplot.CollisionHandler] for path labels.
|
|
57
|
+
raise_on_below_horizon: If True, then a ValueError will be raised if the target is below the horizon at the observing time/location
|
|
58
|
+
scale: Scaling factor that will be applied to all sizes in styles (e.g. font size, marker size, line widths, etc). For example, if you want to make everything 2x bigger, then set the scale to 2. At `scale=1` and `resolution=4096` (the default), all sizes are optimized visually for a map that covers 1-3 constellations. So, if you're creating a plot of a _larger_ extent, then it'd probably be good to decrease the scale (i.e. make everything smaller) -- and _increase_ the scale if you're plotting a very small area.
|
|
59
|
+
autoscale: If True, then the scale will be set automatically based on resolution.
|
|
60
|
+
suppress_warnings: If True (the default), then all warnings will be suppressed
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
OpticPlot: A new instance of an OpticPlot
|
|
64
|
+
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
_coordinate_system = CoordinateSystem.AZ_ALT
|
|
68
|
+
_gradient_direction = GradientDirection.RADIAL
|
|
69
|
+
|
|
70
|
+
FIELD_OF_VIEW_MAX = 20
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
ra: float,
|
|
75
|
+
dec: float,
|
|
76
|
+
optic: Optic,
|
|
77
|
+
observer: Observer = None,
|
|
78
|
+
ephemeris: str = "de421.bsp",
|
|
79
|
+
style: PlotStyle = None,
|
|
80
|
+
resolution: int = 4096,
|
|
81
|
+
point_label_handler: CollisionHandler = None,
|
|
82
|
+
area_label_handler: CollisionHandler = None,
|
|
83
|
+
path_label_handler: CollisionHandler = None,
|
|
84
|
+
raise_on_below_horizon: bool = True,
|
|
85
|
+
scale: float = 1.0,
|
|
86
|
+
autoscale: bool = False,
|
|
87
|
+
suppress_warnings: bool = True,
|
|
88
|
+
*args,
|
|
89
|
+
**kwargs,
|
|
90
|
+
) -> "OpticPlot":
|
|
91
|
+
observer = observer or Observer()
|
|
92
|
+
style = style or PlotStyle().extend(extensions.OPTIC)
|
|
93
|
+
|
|
94
|
+
super().__init__(
|
|
95
|
+
observer,
|
|
96
|
+
ephemeris,
|
|
97
|
+
style,
|
|
98
|
+
resolution,
|
|
99
|
+
point_label_handler=point_label_handler,
|
|
100
|
+
area_label_handler=area_label_handler,
|
|
101
|
+
path_label_handler=path_label_handler,
|
|
102
|
+
scale=scale,
|
|
103
|
+
autoscale=autoscale,
|
|
104
|
+
suppress_warnings=suppress_warnings,
|
|
105
|
+
*args,
|
|
106
|
+
**kwargs,
|
|
107
|
+
)
|
|
108
|
+
self.logger.debug("Creating OpticPlot...")
|
|
109
|
+
|
|
110
|
+
if isinstance(optic, Camera) and style.has_gradient_background():
|
|
111
|
+
raise ValueError("Gradient backgrounds are not yet supported for cameras.")
|
|
112
|
+
|
|
113
|
+
self.ra = ra
|
|
114
|
+
self.dec = dec
|
|
115
|
+
self.raise_on_below_horizon = raise_on_below_horizon
|
|
116
|
+
|
|
117
|
+
self.optic = optic
|
|
118
|
+
self._crs = ccrs.CRS(
|
|
119
|
+
proj4_params=[
|
|
120
|
+
("proj", "latlong"),
|
|
121
|
+
("a", "6378137"),
|
|
122
|
+
],
|
|
123
|
+
globe=ccrs.Globe(ellipse="sphere", flattening=0),
|
|
124
|
+
)
|
|
125
|
+
if self.optic.true_fov > self.FIELD_OF_VIEW_MAX:
|
|
126
|
+
raise ValueError(
|
|
127
|
+
f"Field of View too big: {self.optic.true_fov} (max = {self.FIELD_OF_VIEW_MAX}). Tip: Use horizon or map plots for wider fields of view."
|
|
128
|
+
)
|
|
129
|
+
self._calc_position()
|
|
130
|
+
self._adjust_radec_minmax()
|
|
131
|
+
self._init_plot()
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def alt(self):
|
|
135
|
+
"""Altitude of target (degrees)"""
|
|
136
|
+
return self.pos_alt
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def az(self):
|
|
140
|
+
"""Azimuth of target (degrees)"""
|
|
141
|
+
return self.pos_az
|
|
142
|
+
|
|
143
|
+
def _prepare_coords(self, ra, dec) -> (float, float):
|
|
144
|
+
"""Converts RA/DEC to AZ/ALT"""
|
|
145
|
+
return self.observer._apparent(
|
|
146
|
+
obj=SkyfieldStar(ra_hours=ra / 15, dec_degrees=dec),
|
|
147
|
+
ephemeris=self.ephemeris_name,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def _prepare_coords_many(
|
|
151
|
+
self, coordinates: list, epoch_year: float = 2000
|
|
152
|
+
) -> (float, float):
|
|
153
|
+
"""Converts RA/DEC to AZ/ALT"""
|
|
154
|
+
df = pd.DataFrame(coordinates, columns=["ra", "dec"])
|
|
155
|
+
df["ra_hours"], df["dec_degrees"] = (df.ra / 15, df.dec)
|
|
156
|
+
df["epoch_year"] = epoch_year
|
|
157
|
+
|
|
158
|
+
sf_star = SkyfieldStar.from_dataframe(df)
|
|
159
|
+
|
|
160
|
+
df["x"], df["y"] = self.observer._apparent(
|
|
161
|
+
obj=sf_star,
|
|
162
|
+
ephemeris=self.ephemeris_name,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
return list(zip(df["x"], df["y"]))
|
|
166
|
+
|
|
167
|
+
def _plot_kwargs(self) -> dict:
|
|
168
|
+
return dict(transform=self._crs)
|
|
169
|
+
|
|
170
|
+
def in_bounds(self, ra, dec) -> bool:
|
|
171
|
+
"""Determine if a coordinate is within the bounds of the plot.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
ra: Right ascension, in degrees (0...360)
|
|
175
|
+
dec: Declination, in degrees (-90...90)
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
True if the coordinate is in bounds, otherwise False
|
|
179
|
+
"""
|
|
180
|
+
az, alt = self._prepare_coords(ra, dec)
|
|
181
|
+
return self.in_bounds_altaz(alt, az)
|
|
182
|
+
|
|
183
|
+
def in_bounds_altaz(self, alt, az, scale: float = 1) -> bool:
|
|
184
|
+
"""Determine if a coordinate is within the bounds of the plot.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
alt: Altitude angle in degrees (0...90)
|
|
188
|
+
az: Azimuth angle in degrees (0...360)
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
True if the coordinate is in bounds, otherwise False
|
|
192
|
+
"""
|
|
193
|
+
x, y = self._proj.transform_point(az, alt, self._crs)
|
|
194
|
+
return self.optic.in_bounds(x, y, scale)
|
|
195
|
+
|
|
196
|
+
def _polygon(self, points, style, **kwargs):
|
|
197
|
+
super()._polygon(points, style, transform=self._crs, **kwargs)
|
|
198
|
+
|
|
199
|
+
def _calc_position(self):
|
|
200
|
+
self.observe = self.observer.observe(self.ephemeris_name)
|
|
201
|
+
|
|
202
|
+
target = SkyfieldStar(ra_hours=self.ra / 15, dec_degrees=self.dec)
|
|
203
|
+
self.pos_az, self.pos_alt = self.observer._apparent(
|
|
204
|
+
obj=target,
|
|
205
|
+
ephemeris=self.ephemeris_name,
|
|
206
|
+
)
|
|
207
|
+
if self.pos_alt < 0 and self.raise_on_below_horizon:
|
|
208
|
+
raise ValueError("Target is below horizon at specified time/location.")
|
|
209
|
+
|
|
210
|
+
def _adjust_radec_minmax(self):
|
|
211
|
+
fov = self.optic.true_fov
|
|
212
|
+
extent = geometry.rectangle(
|
|
213
|
+
center=(self.ra, self.dec),
|
|
214
|
+
height_degrees=fov,
|
|
215
|
+
width_degrees=fov,
|
|
216
|
+
)
|
|
217
|
+
minx, miny, maxx, maxy = extent.bounds
|
|
218
|
+
self.ra_min = minx
|
|
219
|
+
self.ra_max = maxx
|
|
220
|
+
self.dec_min = miny
|
|
221
|
+
self.dec_max = maxy
|
|
222
|
+
|
|
223
|
+
if self.ra_max < 0:
|
|
224
|
+
self.ra_max += 360
|
|
225
|
+
if self.ra_min < 0:
|
|
226
|
+
self.ra_min += 360
|
|
227
|
+
|
|
228
|
+
# handle wrapping
|
|
229
|
+
if self.ra_max < self.ra_min:
|
|
230
|
+
self.ra_max += 360
|
|
231
|
+
|
|
232
|
+
if self.dec > self.dec_max:
|
|
233
|
+
self.dec_max = 90
|
|
234
|
+
self.ra_min = 0
|
|
235
|
+
self.ra_max = 360
|
|
236
|
+
|
|
237
|
+
if self.dec < self.dec_min:
|
|
238
|
+
self.dec_min = -90
|
|
239
|
+
self.ra_min = 0
|
|
240
|
+
self.ra_max = 360
|
|
241
|
+
|
|
242
|
+
self.logger.debug(
|
|
243
|
+
f"Extent = RA ({self.ra_min:.2f}, {self.ra_max:.2f}) DEC ({self.dec_min:.2f}, {self.dec_max:.2f})"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
def _in_bounds_xy(self, x: float, y: float) -> bool:
|
|
247
|
+
return self.in_bounds_altaz(y, x) # alt = y, az = x
|
|
248
|
+
|
|
249
|
+
def _prepare_star_coords(self, df):
|
|
250
|
+
df["x"], df["y"] = self.observer._apparent(
|
|
251
|
+
obj=SkyfieldStar.from_dataframe(df),
|
|
252
|
+
ephemeris=self.ephemeris_name,
|
|
253
|
+
)
|
|
254
|
+
return df
|
|
255
|
+
|
|
256
|
+
def _scatter_stars(self, ras, decs, sizes, alphas, colors, style=None, **kwargs):
|
|
257
|
+
plotted = super()._scatter_stars(
|
|
258
|
+
ras, decs, sizes, alphas, colors, style, **kwargs
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
if isinstance(self._background_clip_path, patches.Rectangle):
|
|
262
|
+
# convert to generic path to handle possible rotation angle:
|
|
263
|
+
clip_path = path.Path(self._background_clip_path.get_corners())
|
|
264
|
+
plotted.set_clip_path(clip_path, transform=self.ax.transData)
|
|
265
|
+
else:
|
|
266
|
+
plotted.set_clip_path(self._background_clip_path)
|
|
267
|
+
|
|
268
|
+
@use_style(ObjectStyle, "star")
|
|
269
|
+
def stars(
|
|
270
|
+
self,
|
|
271
|
+
where: list = None,
|
|
272
|
+
where_labels: list = None,
|
|
273
|
+
catalog: Catalog = BIG_SKY_MAG11,
|
|
274
|
+
style: ObjectStyle = None,
|
|
275
|
+
size_fn: Callable[[Star], float] = callables.size_by_magnitude_for_optic,
|
|
276
|
+
alpha_fn: Callable[[Star], float] = callables.alpha_by_magnitude,
|
|
277
|
+
color_fn: Callable[[Star], str] = None,
|
|
278
|
+
label_fn: Callable[[Star], str] = Star.get_label,
|
|
279
|
+
legend_label: str = "Star",
|
|
280
|
+
bayer_labels: bool = False,
|
|
281
|
+
flamsteed_labels: bool = False,
|
|
282
|
+
sql: str = None,
|
|
283
|
+
sql_labels: str = None,
|
|
284
|
+
collision_handler: CollisionHandler = None,
|
|
285
|
+
):
|
|
286
|
+
"""
|
|
287
|
+
Plots stars
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
where: A list of expressions that determine which stars to plot. See [Selecting Objects](/reference-selecting-objects/) for details.
|
|
291
|
+
where_labels: A list of expressions that determine which stars are labeled on the plot. See [Selecting Objects](/reference-selecting-objects/) for details.
|
|
292
|
+
catalog: The catalog of stars to use -- see [catalogs overview](/data/overview/) for details
|
|
293
|
+
style: If `None`, then the plot's style for stars will be used
|
|
294
|
+
size_fn: Callable for calculating the marker size of each star. If `None`, then the marker style's size will be used.
|
|
295
|
+
alpha_fn: Callable for calculating the alpha value (aka "opacity") of each star. If `None`, then the marker style's alpha will be used.
|
|
296
|
+
color_fn: Callable for calculating the color of each star. If `None`, then the marker style's color will be used.
|
|
297
|
+
label_fn: Callable for determining the label of each star.
|
|
298
|
+
legend_label: Label for stars in the legend. If `None`, then they will not be in the legend.
|
|
299
|
+
bayer_labels: If True, then Bayer labels for stars will be plotted.
|
|
300
|
+
flamsteed_labels: If True, then Flamsteed number labels for stars will be plotted.
|
|
301
|
+
sql: SQL query for selecting stars (table name is `_`). This query will be applied _after_ any filters in the `where` kwarg.
|
|
302
|
+
sql_labels: SQL query for selecting stars that will be labeled (table name is `_`). Applied _after_ any filters in the `where_labels` kwarg.
|
|
303
|
+
collision_handler: An instance of [CollisionHandler][starplot.CollisionHandler] that describes what to do on collisions with other labels, markers, etc. If `None`, then the collision handler of the plot will be used.
|
|
304
|
+
"""
|
|
305
|
+
optic_star_multiplier = self.FIELD_OF_VIEW_MAX / self.optic.true_fov
|
|
306
|
+
size_fn_mx = None
|
|
307
|
+
|
|
308
|
+
if size_fn is not None:
|
|
309
|
+
|
|
310
|
+
def size_fn_mx(s):
|
|
311
|
+
return size_fn(s) * optic_star_multiplier * 0.68
|
|
312
|
+
|
|
313
|
+
super().stars(
|
|
314
|
+
where=where,
|
|
315
|
+
where_labels=where_labels,
|
|
316
|
+
catalog=catalog,
|
|
317
|
+
style=style,
|
|
318
|
+
size_fn=size_fn_mx,
|
|
319
|
+
alpha_fn=alpha_fn,
|
|
320
|
+
color_fn=color_fn,
|
|
321
|
+
label_fn=label_fn,
|
|
322
|
+
legend_label=legend_label,
|
|
323
|
+
bayer_labels=bayer_labels,
|
|
324
|
+
flamsteed_labels=flamsteed_labels,
|
|
325
|
+
sql=sql,
|
|
326
|
+
sql_labels=sql_labels,
|
|
327
|
+
collision_handler=collision_handler,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
@use_style(LabelStyle, "info_text")
|
|
331
|
+
def info(self, style: LabelStyle = None):
|
|
332
|
+
"""
|
|
333
|
+
Plots a table with info about the plot, including:
|
|
334
|
+
|
|
335
|
+
- Target's position (alt/az and ra/dec)
|
|
336
|
+
- Observer's position (lat/lon and date/time)
|
|
337
|
+
- Optic details (type, magnification, FOV)
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
style: If `None`, then the plot's style for info text will be used
|
|
341
|
+
"""
|
|
342
|
+
self.ax.set_xlim(-1.22 * self.optic.xlim, 1.22 * self.optic.xlim)
|
|
343
|
+
self.ax.set_ylim(-1.1 * self.optic.ylim, 1.1 * self.optic.ylim)
|
|
344
|
+
self.optic.transform(
|
|
345
|
+
self.ax
|
|
346
|
+
) # apply transform again because new xy limits will undo the transform
|
|
347
|
+
|
|
348
|
+
dt_str = (
|
|
349
|
+
self.observer.dt.strftime("%m/%d/%Y @ %H:%M:%S")
|
|
350
|
+
+ " "
|
|
351
|
+
+ self.observer.dt.tzname()
|
|
352
|
+
)
|
|
353
|
+
font_size = style.font_size * self.scale
|
|
354
|
+
|
|
355
|
+
column_labels = [
|
|
356
|
+
"Target (Alt/Az)",
|
|
357
|
+
"Target (RA/DEC)",
|
|
358
|
+
"Observer Lat, Lon",
|
|
359
|
+
"Observer Date/Time",
|
|
360
|
+
f"Optic - {self.optic.label}",
|
|
361
|
+
]
|
|
362
|
+
values = [
|
|
363
|
+
f"{self.pos_alt:.0f}\N{DEGREE SIGN} / {self.pos_az:.0f}\N{DEGREE SIGN} ({azimuth_to_string(self.pos_az)})",
|
|
364
|
+
f"{(self.ra / 15):.2f}h / {self.dec:.2f}\N{DEGREE SIGN}",
|
|
365
|
+
f"{self.observer.lat:.2f}\N{DEGREE SIGN}, {self.observer.lon:.2f}\N{DEGREE SIGN}",
|
|
366
|
+
dt_str,
|
|
367
|
+
str(self.optic),
|
|
368
|
+
]
|
|
369
|
+
widths = [0.15, 0.15, 0.2, 0.2, 0.3]
|
|
370
|
+
|
|
371
|
+
table = self.ax.table(
|
|
372
|
+
cellText=[values],
|
|
373
|
+
cellLoc="center",
|
|
374
|
+
colWidths=widths,
|
|
375
|
+
rowLabels=[None],
|
|
376
|
+
colLabels=column_labels,
|
|
377
|
+
loc="bottom",
|
|
378
|
+
edges="vertical",
|
|
379
|
+
)
|
|
380
|
+
table.auto_set_font_size(False)
|
|
381
|
+
table.set_fontsize(style.font_size)
|
|
382
|
+
table.scale(1, 5)
|
|
383
|
+
|
|
384
|
+
# Apply style to all cells
|
|
385
|
+
for row in [0, 1]:
|
|
386
|
+
for col in range(len(values)):
|
|
387
|
+
table[row, col].set_text_props(**style.matplot_kwargs(self.scale))
|
|
388
|
+
|
|
389
|
+
# Apply some styles only to the header row
|
|
390
|
+
for col in range(len(values)):
|
|
391
|
+
table[0, col].set_text_props(fontweight="heavy", fontsize=font_size * 1.2)
|
|
392
|
+
|
|
393
|
+
def _plot_border(self):
|
|
394
|
+
# since we're using AzimuthalEquidistant projection, the center will always be (0, 0)
|
|
395
|
+
x = 0
|
|
396
|
+
y = 0
|
|
397
|
+
|
|
398
|
+
if self.style.has_gradient_background():
|
|
399
|
+
background_color = "#ffffff00"
|
|
400
|
+
# self._plot_gradient_background(self.style.background_color)
|
|
401
|
+
else:
|
|
402
|
+
background_color = self.style.background_color.as_hex()
|
|
403
|
+
|
|
404
|
+
# Background of Viewable Area
|
|
405
|
+
self._background_clip_path = self.optic.patch(
|
|
406
|
+
x,
|
|
407
|
+
y,
|
|
408
|
+
facecolor=background_color,
|
|
409
|
+
linewidth=0,
|
|
410
|
+
fill=True,
|
|
411
|
+
zorder=ZOrderEnum.LAYER_1,
|
|
412
|
+
)
|
|
413
|
+
self.ax.set_facecolor(background_color)
|
|
414
|
+
self.ax.add_patch(self._background_clip_path)
|
|
415
|
+
self._update_clip_path_polygon(buffer=15)
|
|
416
|
+
|
|
417
|
+
# Inner Border
|
|
418
|
+
# inner_border = self.optic.patch(
|
|
419
|
+
# x,
|
|
420
|
+
# y,
|
|
421
|
+
# linewidth=2 * self.scale,
|
|
422
|
+
# edgecolor=self.style.border_line_color.as_hex(),
|
|
423
|
+
# fill=False,
|
|
424
|
+
# zorder=ZOrderEnum.LAYER_5 + 100,
|
|
425
|
+
# )
|
|
426
|
+
# self.ax.add_patch(inner_border)
|
|
427
|
+
|
|
428
|
+
if self.style.has_gradient_background():
|
|
429
|
+
self._plot_gradient_background(self.style.background_color)
|
|
430
|
+
|
|
431
|
+
# Outer border
|
|
432
|
+
outer_border = self.optic.patch(
|
|
433
|
+
x,
|
|
434
|
+
y,
|
|
435
|
+
padding=0.05,
|
|
436
|
+
linewidth=25 * self.scale,
|
|
437
|
+
edgecolor=self.style.border_bg_color.as_hex(),
|
|
438
|
+
fill=False,
|
|
439
|
+
zorder=ZOrderEnum.LAYER_5,
|
|
440
|
+
)
|
|
441
|
+
self.ax.add_patch(outer_border)
|
|
442
|
+
|
|
443
|
+
def _init_plot(self):
|
|
444
|
+
self._proj = ccrs.AzimuthalEquidistant(
|
|
445
|
+
central_longitude=self.pos_az,
|
|
446
|
+
central_latitude=self.pos_alt,
|
|
447
|
+
)
|
|
448
|
+
self._proj.threshold = 1000
|
|
449
|
+
self.fig = plt.figure(
|
|
450
|
+
figsize=(self.figure_size, self.figure_size),
|
|
451
|
+
facecolor=self.style.figure_background_color.as_hex(),
|
|
452
|
+
# layout="constrained",
|
|
453
|
+
dpi=DPI,
|
|
454
|
+
)
|
|
455
|
+
self.ax = self.fig.add_subplot(1, 1, 1, projection=self._proj)
|
|
456
|
+
self.fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
|
|
457
|
+
|
|
458
|
+
self.ax.xaxis.set_visible(False)
|
|
459
|
+
self.ax.yaxis.set_visible(False)
|
|
460
|
+
self.ax.axis("off")
|
|
461
|
+
|
|
462
|
+
self._fit_to_ax()
|
|
463
|
+
self.ax.set_xlim(-1.06 * self.optic.xlim, 1.06 * self.optic.xlim)
|
|
464
|
+
self.ax.set_ylim(-1.06 * self.optic.ylim, 1.06 * self.optic.ylim)
|
|
465
|
+
self.optic.transform(self.ax)
|
|
466
|
+
self._plot_border()
|
|
467
|
+
|
|
468
|
+
# if self.gradient_preset:
|
|
469
|
+
# self.apply_gradient_background(self.gradient_preset)
|
starplot/plots/zenith.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from matplotlib import path, patches
|
|
3
|
+
|
|
4
|
+
from starplot.coordinates import CoordinateSystem
|
|
5
|
+
from starplot.data.translations import translate
|
|
6
|
+
from starplot.plots.map import MapPlot
|
|
7
|
+
from starplot.models.observer import Observer
|
|
8
|
+
from starplot.projections import Stereographic
|
|
9
|
+
from starplot.styles import (
|
|
10
|
+
LabelStyle,
|
|
11
|
+
PlotStyle,
|
|
12
|
+
PathStyle,
|
|
13
|
+
GradientDirection,
|
|
14
|
+
extensions,
|
|
15
|
+
)
|
|
16
|
+
from starplot.styles.helpers import use_style
|
|
17
|
+
from starplot.plotters.text import CollisionHandler
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ZenithPlot(MapPlot):
|
|
21
|
+
"""Creates a new zenith plot.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
observer: Observer instance which specifies a time and place. Defaults to `Observer()`
|
|
25
|
+
ephemeris: Ephemeris to use for calculating planet positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
|
|
26
|
+
style: Styling for the plot (colors, sizes, fonts, etc). If `None`, it defaults to `PlotStyle()`
|
|
27
|
+
resolution: Size (in pixels) of largest dimension of the map
|
|
28
|
+
point_label_handler: Default [CollisionHandler][starplot.CollisionHandler] for point labels.
|
|
29
|
+
area_label_handler: Default [CollisionHandler][starplot.CollisionHandler] for area labels.
|
|
30
|
+
path_label_handler: Default [CollisionHandler][starplot.CollisionHandler] for path labels.
|
|
31
|
+
scale: Scaling factor that will be applied to all sizes in styles (e.g. font size, marker size, line widths, etc). For example, if you want to make everything 2x bigger, then set the scale to 2. At `scale=1` and `resolution=4096` (the default), all sizes are optimized visually for a map that covers 1-3 constellations. So, if you're creating a plot of a _larger_ extent, then it'd probably be good to decrease the scale (i.e. make everything smaller) -- and _increase_ the scale if you're plotting a very small area.
|
|
32
|
+
autoscale: If True, then the scale will be set automatically based on resolution.
|
|
33
|
+
suppress_warnings: If True (the default), then all warnings will be suppressed
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
ZenithPlot: A new instance of a ZenithPlot
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
_coordinate_system = CoordinateSystem.RA_DEC
|
|
41
|
+
_gradient_direction = GradientDirection.RADIAL
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
observer: Observer = None,
|
|
46
|
+
ephemeris: str = "de421.bsp",
|
|
47
|
+
style: PlotStyle = None,
|
|
48
|
+
resolution: int = 4096,
|
|
49
|
+
point_label_handler: CollisionHandler = None,
|
|
50
|
+
area_label_handler: CollisionHandler = None,
|
|
51
|
+
path_label_handler: CollisionHandler = None,
|
|
52
|
+
scale: float = 1.0,
|
|
53
|
+
autoscale: bool = False,
|
|
54
|
+
suppress_warnings: bool = True,
|
|
55
|
+
*args,
|
|
56
|
+
**kwargs,
|
|
57
|
+
) -> "ZenithPlot":
|
|
58
|
+
observer = observer or Observer()
|
|
59
|
+
projection = Stereographic(
|
|
60
|
+
center_ra=observer.lst,
|
|
61
|
+
center_dec=observer.lat,
|
|
62
|
+
)
|
|
63
|
+
style = style or PlotStyle().extend(extensions.MAP)
|
|
64
|
+
|
|
65
|
+
super().__init__(
|
|
66
|
+
projection,
|
|
67
|
+
0,
|
|
68
|
+
360,
|
|
69
|
+
-90,
|
|
70
|
+
90,
|
|
71
|
+
observer,
|
|
72
|
+
ephemeris,
|
|
73
|
+
style,
|
|
74
|
+
resolution,
|
|
75
|
+
point_label_handler=point_label_handler,
|
|
76
|
+
area_label_handler=area_label_handler,
|
|
77
|
+
path_label_handler=path_label_handler,
|
|
78
|
+
clip_path=None,
|
|
79
|
+
scale=scale,
|
|
80
|
+
autoscale=autoscale,
|
|
81
|
+
suppress_warnings=suppress_warnings,
|
|
82
|
+
*args,
|
|
83
|
+
**kwargs,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
@use_style(PathStyle, "horizon")
|
|
87
|
+
def horizon(
|
|
88
|
+
self,
|
|
89
|
+
style: PathStyle = None,
|
|
90
|
+
labels: list = ["N", "E", "S", "W"],
|
|
91
|
+
):
|
|
92
|
+
"""
|
|
93
|
+
Draws a [great circle](https://en.wikipedia.org/wiki/Great_circle) representing the horizon for the given `lat`, `lon` at time `dt` (so you must define these when creating the plot to use this function)
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
style: Style of the horizon path. If None, then the plot's style definition will be used.
|
|
97
|
+
labels: List of labels for cardinal directions. **NOTE: labels should be in the order: North, East, South, West.**
|
|
98
|
+
"""
|
|
99
|
+
if self.observer is None:
|
|
100
|
+
raise ValueError("observer is required for plotting the horizon")
|
|
101
|
+
|
|
102
|
+
"""
|
|
103
|
+
For zenith projections, we plot the horizon as a patch to make a more perfect circle
|
|
104
|
+
"""
|
|
105
|
+
style_kwargs = style.line.matplot_kwargs(self.scale)
|
|
106
|
+
style_kwargs["clip_on"] = False
|
|
107
|
+
style_kwargs["edgecolor"] = style_kwargs.pop("color")
|
|
108
|
+
patch = patches.Circle(
|
|
109
|
+
(0.50, 0.50),
|
|
110
|
+
radius=0.454,
|
|
111
|
+
facecolor=None,
|
|
112
|
+
fill=False,
|
|
113
|
+
transform=self.ax.transAxes,
|
|
114
|
+
**style_kwargs,
|
|
115
|
+
)
|
|
116
|
+
self.ax.add_patch(patch)
|
|
117
|
+
self._background_clip_path = patch
|
|
118
|
+
self._update_clip_path_polygon(
|
|
119
|
+
buffer=style.line.width / 2 + 2 * style.line.edge_width + 40
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if not labels:
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
labels = [translate(label, self.language) for label in labels]
|
|
126
|
+
|
|
127
|
+
label_ax_coords = [
|
|
128
|
+
(0.5, 0.95), # north
|
|
129
|
+
(0.045, 0.5), # east
|
|
130
|
+
(0.5, 0.045), # south
|
|
131
|
+
(0.954, 0.5), # west
|
|
132
|
+
]
|
|
133
|
+
for label, coords in zip(labels, label_ax_coords):
|
|
134
|
+
self.ax.annotate(
|
|
135
|
+
label,
|
|
136
|
+
coords,
|
|
137
|
+
xycoords=self.ax.transAxes,
|
|
138
|
+
clip_on=False,
|
|
139
|
+
**style.label.matplot_kwargs(self.scale),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def _adjust_radec_minmax(self):
|
|
143
|
+
self.ra_min = 0
|
|
144
|
+
self.ra_max = 360
|
|
145
|
+
self.dec_min = -90
|
|
146
|
+
self.dec_max = 90
|
|
147
|
+
|
|
148
|
+
def _set_extent(self):
|
|
149
|
+
theta = np.linspace(0, 2 * np.pi, 100)
|
|
150
|
+
center, radius = [0.5, 0.5], 0.45
|
|
151
|
+
verts = np.vstack([np.sin(theta), np.cos(theta)]).T
|
|
152
|
+
circle = path.Path(verts * radius + center)
|
|
153
|
+
extent = self.ax.get_extent(crs=self._proj)
|
|
154
|
+
self.ax.set_extent((p / 3.548 for p in extent), crs=self._proj)
|
|
155
|
+
self.ax.set_boundary(circle, transform=self.ax.transAxes)
|
|
156
|
+
|
|
157
|
+
@use_style(LabelStyle, "info_text")
|
|
158
|
+
def info(self, style: LabelStyle = None):
|
|
159
|
+
"""
|
|
160
|
+
Plots info text in the lower left corner, including date/time and lat/lon.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
style: Styling of the info text. If None, then the plot's style definition will be used.
|
|
164
|
+
"""
|
|
165
|
+
dt_str = self.dt.strftime("%m/%d/%Y @ %H:%M:%S") + " " + self.dt.tzname()
|
|
166
|
+
info = f"{str(self.observer.lat)}, {str(self.observer.lon)}\n{dt_str}"
|
|
167
|
+
self.ax.text(
|
|
168
|
+
0.05,
|
|
169
|
+
0.05,
|
|
170
|
+
info,
|
|
171
|
+
transform=self.ax.transAxes,
|
|
172
|
+
**style.matplot_kwargs(self.scale),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def _plot_background_clip_path(self):
|
|
176
|
+
if self.style.has_gradient_background():
|
|
177
|
+
background_color = "#ffffff00"
|
|
178
|
+
self._plot_gradient_background(self.style.background_color)
|
|
179
|
+
else:
|
|
180
|
+
background_color = self.style.background_color.as_hex()
|
|
181
|
+
|
|
182
|
+
self._background_clip_path = patches.Circle(
|
|
183
|
+
(0.50, 0.50),
|
|
184
|
+
radius=0.45,
|
|
185
|
+
fill=True,
|
|
186
|
+
facecolor=background_color,
|
|
187
|
+
# edgecolor=self.style.border_line_color.as_hex(),
|
|
188
|
+
linewidth=0,
|
|
189
|
+
zorder=-2_000,
|
|
190
|
+
transform=self.ax.transAxes,
|
|
191
|
+
)
|
|
192
|
+
self.ax.set_facecolor(background_color)
|
|
193
|
+
|
|
194
|
+
self.ax.add_patch(self._background_clip_path)
|
|
195
|
+
self._update_clip_path_polygon(buffer=20)
|
|
196
|
+
|
|
197
|
+
def _prepare_star_coords(self, df, limit_by_altaz=False):
|
|
198
|
+
# TODO : reconcile this commented code
|
|
199
|
+
# self.location = self.earth + wgs84.latlon(
|
|
200
|
+
# self.observer.lat, self.observer.lon
|
|
201
|
+
# )
|
|
202
|
+
# df["ra_hours"], df["dec_degrees"] = (df.ra / 15, df.dec)
|
|
203
|
+
# stars_apparent = (
|
|
204
|
+
# self.location.at(self.observer.timescale)
|
|
205
|
+
# .observe(SkyfieldStar.from_dataframe(df))
|
|
206
|
+
# .apparent()
|
|
207
|
+
# )
|
|
208
|
+
# # we only need altitude
|
|
209
|
+
# stars_alt, _, _ = stars_apparent.altaz()
|
|
210
|
+
# df["alt"] = stars_alt.degrees
|
|
211
|
+
# df = df[df["alt"] > 0]
|
|
212
|
+
|
|
213
|
+
df["x"], df["y"] = (
|
|
214
|
+
df["ra"],
|
|
215
|
+
df["dec"],
|
|
216
|
+
)
|
|
217
|
+
return df
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# ruff: noqa: F401,F403
|
|
2
|
+
|
|
3
|
+
from .constellations import ConstellationPlotterMixin
|
|
4
|
+
from .stars import StarPlotterMixin
|
|
5
|
+
from .dsos import DsoPlotterMixin
|
|
6
|
+
from .milkyway import MilkyWayPlotterMixin
|
|
7
|
+
from .legend import LegendPlotterMixin
|
|
8
|
+
from .gradients import GradientBackgroundMixin
|
|
9
|
+
from .arrow import ArrowPlotterMixin
|