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
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
from functools import cache
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from cartopy import crs as ccrs
|
|
9
|
+
from matplotlib import pyplot as plt, patches
|
|
10
|
+
from matplotlib.ticker import FixedLocator, FuncFormatter
|
|
11
|
+
from skyfield.api import Star as SkyfieldStar
|
|
12
|
+
from shapely import Polygon, MultiPolygon
|
|
13
|
+
from starplot.coordinates import CoordinateSystem
|
|
14
|
+
from starplot.plots.base import BasePlot, DPI
|
|
15
|
+
from starplot.mixins import ExtentMaskMixin
|
|
16
|
+
from starplot.models.observer import Observer
|
|
17
|
+
from starplot.plotters import (
|
|
18
|
+
ConstellationPlotterMixin,
|
|
19
|
+
StarPlotterMixin,
|
|
20
|
+
DsoPlotterMixin,
|
|
21
|
+
MilkyWayPlotterMixin,
|
|
22
|
+
GradientBackgroundMixin,
|
|
23
|
+
LegendPlotterMixin,
|
|
24
|
+
ArrowPlotterMixin,
|
|
25
|
+
)
|
|
26
|
+
from starplot.plotters.text import CollisionHandler
|
|
27
|
+
from starplot.styles import (
|
|
28
|
+
PlotStyle,
|
|
29
|
+
extensions,
|
|
30
|
+
use_style,
|
|
31
|
+
PathStyle,
|
|
32
|
+
GradientDirection,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
DEFAULT_HORIZON_LABELS = {
|
|
36
|
+
0: "N",
|
|
37
|
+
45: "NE",
|
|
38
|
+
90: "E",
|
|
39
|
+
135: "SE",
|
|
40
|
+
180: "S",
|
|
41
|
+
225: "SW",
|
|
42
|
+
270: "W",
|
|
43
|
+
315: "NW",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class HorizonPlot(
|
|
48
|
+
BasePlot,
|
|
49
|
+
ExtentMaskMixin,
|
|
50
|
+
# HorizonExtentMaskMixin,
|
|
51
|
+
ConstellationPlotterMixin,
|
|
52
|
+
StarPlotterMixin,
|
|
53
|
+
DsoPlotterMixin,
|
|
54
|
+
MilkyWayPlotterMixin,
|
|
55
|
+
GradientBackgroundMixin,
|
|
56
|
+
LegendPlotterMixin,
|
|
57
|
+
ArrowPlotterMixin,
|
|
58
|
+
):
|
|
59
|
+
"""Creates a new horizon plot.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
altitude: Tuple of altitude range to plot (min, max)
|
|
63
|
+
azimuth: Tuple of azimuth range to plot (min, max)
|
|
64
|
+
observer: Observer instance which specifies a time and place. Defaults to `Observer()`
|
|
65
|
+
ephemeris: Ephemeris to use for calculating planet positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
|
|
66
|
+
style: Styling for the plot (colors, sizes, fonts, etc). If `None`, it defaults to `PlotStyle()`
|
|
67
|
+
resolution: Size (in pixels) of largest dimension of the map
|
|
68
|
+
point_label_handler: Default [CollisionHandler][starplot.CollisionHandler] for point labels.
|
|
69
|
+
area_label_handler: Default [CollisionHandler][starplot.CollisionHandler] for area labels.
|
|
70
|
+
path_label_handler: Default [CollisionHandler][starplot.CollisionHandler] for path labels.
|
|
71
|
+
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.
|
|
72
|
+
autoscale: If True, then the scale will be automatically set based on resolution
|
|
73
|
+
suppress_warnings: If True (the default), then all warnings will be suppressed
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
HorizonPlot: A new instance of an HorizonPlot
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
_coordinate_system = CoordinateSystem.AZ_ALT
|
|
81
|
+
_gradient_direction = GradientDirection.LINEAR
|
|
82
|
+
|
|
83
|
+
FIELD_OF_VIEW_MAX = 9.0
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
altitude: tuple[float, float],
|
|
88
|
+
azimuth: tuple[float, float],
|
|
89
|
+
observer: Observer = None,
|
|
90
|
+
ephemeris: str = "de421.bsp",
|
|
91
|
+
style: PlotStyle = None,
|
|
92
|
+
resolution: int = 4096,
|
|
93
|
+
point_label_handler: CollisionHandler = None,
|
|
94
|
+
area_label_handler: CollisionHandler = None,
|
|
95
|
+
path_label_handler: CollisionHandler = None,
|
|
96
|
+
scale: float = 1.0,
|
|
97
|
+
autoscale: bool = False,
|
|
98
|
+
suppress_warnings: bool = True,
|
|
99
|
+
*args,
|
|
100
|
+
**kwargs,
|
|
101
|
+
) -> "HorizonPlot":
|
|
102
|
+
observer = observer or Observer()
|
|
103
|
+
style = style or PlotStyle().extend(extensions.MAP)
|
|
104
|
+
|
|
105
|
+
super().__init__(
|
|
106
|
+
observer,
|
|
107
|
+
ephemeris,
|
|
108
|
+
style,
|
|
109
|
+
resolution,
|
|
110
|
+
point_label_handler=point_label_handler,
|
|
111
|
+
area_label_handler=area_label_handler,
|
|
112
|
+
path_label_handler=path_label_handler,
|
|
113
|
+
scale=scale,
|
|
114
|
+
autoscale=autoscale,
|
|
115
|
+
suppress_warnings=suppress_warnings,
|
|
116
|
+
*args,
|
|
117
|
+
**kwargs,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if azimuth[0] >= azimuth[1]:
|
|
121
|
+
raise ValueError("Azimuth min must be less than max")
|
|
122
|
+
if azimuth[1] - azimuth[0] > 180:
|
|
123
|
+
raise ValueError("Azimuth range cannot be greater than 180 degrees")
|
|
124
|
+
|
|
125
|
+
if altitude[0] >= altitude[1]:
|
|
126
|
+
raise ValueError("Altitude min must be less than max")
|
|
127
|
+
if altitude[1] - altitude[0] > 90:
|
|
128
|
+
raise ValueError("Altitude range cannot be greater than 90 degrees")
|
|
129
|
+
|
|
130
|
+
self.logger.debug("Creating HorizonPlot...")
|
|
131
|
+
self.alt = altitude
|
|
132
|
+
self.az = azimuth
|
|
133
|
+
self._alt = altitude
|
|
134
|
+
self._az = azimuth
|
|
135
|
+
self.center_alt = sum(altitude) / 2
|
|
136
|
+
self.center_az = sum(azimuth) / 2
|
|
137
|
+
|
|
138
|
+
self._geodetic = ccrs.Geodetic()
|
|
139
|
+
self._plate_carree = ccrs.PlateCarree()
|
|
140
|
+
self._crs = ccrs.CRS(
|
|
141
|
+
proj4_params=[
|
|
142
|
+
("proj", "latlong"),
|
|
143
|
+
("a", "6378137"),
|
|
144
|
+
],
|
|
145
|
+
globe=ccrs.Globe(ellipse="sphere", flattening=0),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
self._init_plot()
|
|
149
|
+
|
|
150
|
+
self.altaz_mask = self._extent_mask_altaz()
|
|
151
|
+
self.logger.debug(f"Extent = AZ ({self.az}) ALT ({self.alt})")
|
|
152
|
+
|
|
153
|
+
self._calc_position()
|
|
154
|
+
|
|
155
|
+
@cache
|
|
156
|
+
def _prepare_coords(self, ra, dec) -> (float, float):
|
|
157
|
+
"""Converts RA/DEC to AZ/ALT"""
|
|
158
|
+
if ra > 360:
|
|
159
|
+
ra -= 360
|
|
160
|
+
if ra < 0:
|
|
161
|
+
ra += 360
|
|
162
|
+
|
|
163
|
+
return self.observer._apparent(
|
|
164
|
+
obj=SkyfieldStar(ra_hours=ra / 15, dec_degrees=dec),
|
|
165
|
+
ephemeris=self.ephemeris_name,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def _prepare_coords_many(
|
|
169
|
+
self, coordinates: list, epoch_year: float = 2000
|
|
170
|
+
) -> (float, float):
|
|
171
|
+
"""Converts RA/DEC to AZ/ALT"""
|
|
172
|
+
df = pd.DataFrame(coordinates, columns=["ra", "dec"])
|
|
173
|
+
df["ra_hours"], df["dec_degrees"] = (df.ra / 15, df.dec)
|
|
174
|
+
df["epoch_year"] = epoch_year
|
|
175
|
+
|
|
176
|
+
sf_star = SkyfieldStar.from_dataframe(df)
|
|
177
|
+
|
|
178
|
+
df["x"], df["y"] = self.observer._apparent(
|
|
179
|
+
obj=sf_star,
|
|
180
|
+
ephemeris=self.ephemeris_name,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return list(zip(df["x"], df["y"]))
|
|
184
|
+
|
|
185
|
+
def _prepare_star_coords(self, df, limit_by_altaz=True):
|
|
186
|
+
df["x"], df["y"] = self.observer._apparent(
|
|
187
|
+
obj=SkyfieldStar.from_dataframe(df),
|
|
188
|
+
ephemeris=self.ephemeris_name,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# if limit_by_altaz:
|
|
192
|
+
# extent = self._extent_mask_altaz()
|
|
193
|
+
# df["_geometry_az_alt"] = gpd.points_from_xy(df.x, df.y)
|
|
194
|
+
# df = df[df["_geometry_az_alt"].intersects(extent)]
|
|
195
|
+
|
|
196
|
+
return df
|
|
197
|
+
|
|
198
|
+
def _calc_position(self):
|
|
199
|
+
self.observe = self.observer.observe(self.ephemeris_name)
|
|
200
|
+
|
|
201
|
+
self.ra_min = 0
|
|
202
|
+
self.ra_max = 360
|
|
203
|
+
self.dec_min = self.observer.lat - 90
|
|
204
|
+
self.dec_max = self.observer.lat + 90
|
|
205
|
+
|
|
206
|
+
self.logger.debug(
|
|
207
|
+
f"Extent = RA ({self.ra_min:.2f}, {self.ra_max:.2f}) DEC ({self.dec_min:.2f}, {self.dec_max:.2f})"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def _plot_kwargs(self) -> dict:
|
|
211
|
+
return dict(transform=self._crs)
|
|
212
|
+
|
|
213
|
+
@cache
|
|
214
|
+
def in_bounds(self, ra, dec) -> bool:
|
|
215
|
+
"""Determine if a coordinate is within the bounds of the plot.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
ra: Right ascension, in hours (0...24)
|
|
219
|
+
dec: Declination, in degrees (-90...90)
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
True if the coordinate is in bounds, otherwise False
|
|
223
|
+
"""
|
|
224
|
+
az, alt = self._prepare_coords(ra, dec)
|
|
225
|
+
return self.in_bounds_altaz(alt, az)
|
|
226
|
+
|
|
227
|
+
def in_bounds_altaz(self, alt, az, scale: float = 1) -> bool:
|
|
228
|
+
"""Determine if a coordinate is within the bounds of the plot.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
alt: Altitude angle in degrees (0...90)
|
|
232
|
+
az: Azimuth angle in degrees (0...360)
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
True if the coordinate is in bounds, otherwise False
|
|
236
|
+
"""
|
|
237
|
+
# return self.altaz_mask.contains(Point(az, alt))
|
|
238
|
+
x, y = self._to_ax(az, alt)
|
|
239
|
+
return 0 <= x <= 1 and 0 <= y <= 1
|
|
240
|
+
|
|
241
|
+
def _in_bounds_xy(self, x: float, y: float) -> bool:
|
|
242
|
+
return self.in_bounds_altaz(y, x) # alt = y, az = x
|
|
243
|
+
|
|
244
|
+
def _polygon(self, points, style, **kwargs):
|
|
245
|
+
super()._polygon(points, style, transform=self._crs, **kwargs)
|
|
246
|
+
|
|
247
|
+
@cache
|
|
248
|
+
def _extent_mask_altaz(self):
|
|
249
|
+
"""
|
|
250
|
+
Returns shapely geometry objects of the alt/az extent
|
|
251
|
+
|
|
252
|
+
If the extent crosses North cardinal direction, then a MultiPolygon will be returned
|
|
253
|
+
"""
|
|
254
|
+
extent = list(self.ax.get_extent(crs=self._plate_carree))
|
|
255
|
+
alt_min, alt_max = extent[2], extent[3]
|
|
256
|
+
az_min, az_max = extent[0], extent[1]
|
|
257
|
+
|
|
258
|
+
az_ul, _ = self._ax_to_azalt(0, 1)
|
|
259
|
+
az_ur, _ = self._ax_to_azalt(1, 1)
|
|
260
|
+
|
|
261
|
+
if az_ul < 0:
|
|
262
|
+
az_ul += 360
|
|
263
|
+
|
|
264
|
+
if az_ur < 0:
|
|
265
|
+
az_ur += 360
|
|
266
|
+
|
|
267
|
+
az_min = min(self.az[0], self.az[1], az_ul, az_ur)
|
|
268
|
+
az_max = max(self.az[0], self.az[1], az_ul, az_ur)
|
|
269
|
+
|
|
270
|
+
if az_min < 0:
|
|
271
|
+
az_min += 360
|
|
272
|
+
if az_max < 0:
|
|
273
|
+
az_max += 360
|
|
274
|
+
|
|
275
|
+
if az_min >= az_max:
|
|
276
|
+
az_max += 360
|
|
277
|
+
|
|
278
|
+
self.az = (az_min, az_max)
|
|
279
|
+
self.alt = (alt_min, alt_max)
|
|
280
|
+
|
|
281
|
+
if az_max <= 360:
|
|
282
|
+
coords = [
|
|
283
|
+
[az_min, alt_min],
|
|
284
|
+
[az_max, alt_min],
|
|
285
|
+
[az_max, alt_max],
|
|
286
|
+
[az_min, alt_max],
|
|
287
|
+
[az_min, alt_min],
|
|
288
|
+
]
|
|
289
|
+
return Polygon(coords)
|
|
290
|
+
|
|
291
|
+
else:
|
|
292
|
+
coords_1 = [
|
|
293
|
+
[az_min, alt_min],
|
|
294
|
+
[360, alt_min],
|
|
295
|
+
[360, alt_max],
|
|
296
|
+
[az_min, alt_max],
|
|
297
|
+
[az_min, alt_min],
|
|
298
|
+
]
|
|
299
|
+
coords_2 = [
|
|
300
|
+
[0, alt_min],
|
|
301
|
+
[az_max - 360, alt_min],
|
|
302
|
+
[az_max - 360, alt_max],
|
|
303
|
+
[0, alt_max],
|
|
304
|
+
[0, alt_min],
|
|
305
|
+
]
|
|
306
|
+
|
|
307
|
+
return MultiPolygon(
|
|
308
|
+
[
|
|
309
|
+
Polygon(coords_1),
|
|
310
|
+
Polygon(coords_2),
|
|
311
|
+
]
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
@use_style(PathStyle, "horizon")
|
|
315
|
+
def horizon(
|
|
316
|
+
self,
|
|
317
|
+
style: PathStyle = None,
|
|
318
|
+
labels: dict[int, str] = DEFAULT_HORIZON_LABELS,
|
|
319
|
+
):
|
|
320
|
+
"""
|
|
321
|
+
Plots rectangle for horizon that shows cardinal directions and azimuth labels.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
style: Style of the horizon path. If None, then the plot's style definition will be used.
|
|
325
|
+
labels: Dictionary that maps azimuth values (0...360) to their cardinal direction labels (e.g. "N"). Default is to label each 45deg direction (e.g. "N", "NE", "E", etc)
|
|
326
|
+
"""
|
|
327
|
+
patch_y = -0.11 * self.scale
|
|
328
|
+
bottom = patches.Polygon(
|
|
329
|
+
[
|
|
330
|
+
(0, -0.04 * self.scale),
|
|
331
|
+
(1, -0.04 * self.scale),
|
|
332
|
+
(1, patch_y),
|
|
333
|
+
(0, patch_y),
|
|
334
|
+
(0, -0.04 * self.scale),
|
|
335
|
+
],
|
|
336
|
+
color=style.line.color.as_hex(),
|
|
337
|
+
transform=self.ax.transAxes,
|
|
338
|
+
clip_on=False,
|
|
339
|
+
)
|
|
340
|
+
self.ax.add_patch(bottom)
|
|
341
|
+
|
|
342
|
+
for az, label in labels.items():
|
|
343
|
+
az = int(az)
|
|
344
|
+
x, _ = self._to_ax(az, self.alt[0])
|
|
345
|
+
if x <= 0.03 or x >= 0.97 or math.isnan(x):
|
|
346
|
+
continue
|
|
347
|
+
|
|
348
|
+
self.ax.annotate(
|
|
349
|
+
label,
|
|
350
|
+
(x, patch_y + 0.027),
|
|
351
|
+
xycoords=self.ax.transAxes,
|
|
352
|
+
xytext=(
|
|
353
|
+
style.label.offset_x * self.scale,
|
|
354
|
+
style.label.offset_y * self.scale,
|
|
355
|
+
),
|
|
356
|
+
textcoords="offset points",
|
|
357
|
+
**style.label.matplot_kwargs(self.scale),
|
|
358
|
+
clip_on=False,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
@use_style(PathStyle, "gridlines")
|
|
362
|
+
def gridlines(
|
|
363
|
+
self,
|
|
364
|
+
style: PathStyle = None,
|
|
365
|
+
show_labels: list = ["left", "right", "bottom"],
|
|
366
|
+
az_locations: list[float] = None,
|
|
367
|
+
alt_locations: list[float] = None,
|
|
368
|
+
az_formatter_fn: Callable[[float], str] = None,
|
|
369
|
+
alt_formatter_fn: Callable[[float], str] = None,
|
|
370
|
+
divider_line: bool = True,
|
|
371
|
+
show_ticks: bool = True,
|
|
372
|
+
tick_step: int = 5,
|
|
373
|
+
):
|
|
374
|
+
"""
|
|
375
|
+
Plots gridlines
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
style: Styling of the gridlines. If None, then the plot's style (specified when creating the plot) will be used
|
|
379
|
+
show_labels: List of locations where labels should be shown (options: "left", "right", "top", "bottom")
|
|
380
|
+
az_locations: List of azimuth locations for the gridlines (in degrees, 0...360). Defaults to every 15 degrees
|
|
381
|
+
alt_locations: List of altitude locations for the gridlines (in degrees, -90...90). Defaults to every 10 degrees.
|
|
382
|
+
az_formatter_fn: Callable for creating labels of azimuth gridlines
|
|
383
|
+
alt_formatter_fn: Callable for creating labels of altitude gridlines
|
|
384
|
+
divider_line: If True, then a divider line will be plotted below the azimuth labels on the bottom of the plot (this is helpful when also plotting the horizon)
|
|
385
|
+
show_ticks: If True, then tick marks will be plotted on the horizon path for every `tick_step` degree that is not also a degree label
|
|
386
|
+
tick_step: Step size for tick marks
|
|
387
|
+
"""
|
|
388
|
+
az_formatter_fn_default = lambda az: f"{round(az)}\u00b0 " # noqa: E731
|
|
389
|
+
alt_formatter_fn_default = lambda alt: f"{round(alt)}\u00b0 " # noqa: E731
|
|
390
|
+
|
|
391
|
+
az_formatter_fn = az_formatter_fn or az_formatter_fn_default
|
|
392
|
+
alt_formatter_fn = alt_formatter_fn or alt_formatter_fn_default
|
|
393
|
+
|
|
394
|
+
def az_formatter(x, pos) -> str:
|
|
395
|
+
if x < 0:
|
|
396
|
+
x += 360
|
|
397
|
+
return az_formatter_fn(x)
|
|
398
|
+
|
|
399
|
+
def alt_formatter(x, pos) -> str:
|
|
400
|
+
return alt_formatter_fn(x)
|
|
401
|
+
|
|
402
|
+
x_locations = az_locations or [x for x in range(0, 360, 15)]
|
|
403
|
+
x_locations = [x - 180 for x in x_locations]
|
|
404
|
+
y_locations = alt_locations or [d for d in range(-90, 90, 10)]
|
|
405
|
+
|
|
406
|
+
label_style_kwargs = style.label.matplot_kwargs(self.scale)
|
|
407
|
+
label_style_kwargs.pop("va")
|
|
408
|
+
label_style_kwargs.pop("ha")
|
|
409
|
+
|
|
410
|
+
line_style_kwargs = style.line.matplot_kwargs(self.scale)
|
|
411
|
+
gridlines = self.ax.gridlines(
|
|
412
|
+
draw_labels=show_labels,
|
|
413
|
+
x_inline=False,
|
|
414
|
+
y_inline=False,
|
|
415
|
+
rotate_labels=False,
|
|
416
|
+
xpadding=12,
|
|
417
|
+
ypadding=12,
|
|
418
|
+
gid="gridlines",
|
|
419
|
+
xlocs=FixedLocator(x_locations),
|
|
420
|
+
xformatter=FuncFormatter(az_formatter),
|
|
421
|
+
xlabel_style=label_style_kwargs,
|
|
422
|
+
ylocs=FixedLocator(y_locations),
|
|
423
|
+
ylabel_style=label_style_kwargs,
|
|
424
|
+
yformatter=FuncFormatter(alt_formatter),
|
|
425
|
+
**line_style_kwargs,
|
|
426
|
+
)
|
|
427
|
+
gridlines.set_zorder(style.line.zorder)
|
|
428
|
+
|
|
429
|
+
if show_labels:
|
|
430
|
+
self._axis_labels = True
|
|
431
|
+
|
|
432
|
+
# gridlines.xlocator = FixedLocator(x_locations)
|
|
433
|
+
# gridlines.xformatter = FuncFormatter(az_formatter)
|
|
434
|
+
# gridlines.xlabel_style = label_style_kwargs
|
|
435
|
+
|
|
436
|
+
# gridlines.ylocator = FixedLocator(y_locations)
|
|
437
|
+
# gridlines.yformatter = FuncFormatter(alt_formatter)
|
|
438
|
+
# gridlines.ylabel_style = label_style_kwargs
|
|
439
|
+
# print(gridlines.label_artists)
|
|
440
|
+
# for label in gridlines.label_artists:
|
|
441
|
+
# label.set_zorder(style.label.zorder)
|
|
442
|
+
|
|
443
|
+
if divider_line:
|
|
444
|
+
self.ax.plot(
|
|
445
|
+
[0, 1],
|
|
446
|
+
[-0.04 * self.scale, -0.04 * self.scale],
|
|
447
|
+
lw=1,
|
|
448
|
+
color=style.label.font_color.as_hex(),
|
|
449
|
+
clip_on=False,
|
|
450
|
+
transform=self.ax.transAxes,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
if not show_ticks or len(x_locations) < 2:
|
|
454
|
+
return
|
|
455
|
+
|
|
456
|
+
# sort x locations so we iterate in order
|
|
457
|
+
x_locations_sorted = sorted(x_locations)
|
|
458
|
+
for i, az in enumerate(x_locations_sorted[1:], start=1):
|
|
459
|
+
prev_az = x_locations_sorted[i - 1]
|
|
460
|
+
|
|
461
|
+
# start at az label location + tick step cause we only want ticks between labels
|
|
462
|
+
for az_tick in range(prev_az + tick_step, az, tick_step):
|
|
463
|
+
a = int(az_tick)
|
|
464
|
+
if a >= 360:
|
|
465
|
+
a -= 360
|
|
466
|
+
x, _ = self._to_ax(a, self.alt[0])
|
|
467
|
+
|
|
468
|
+
if x <= 0.03 or x >= 0.97 or math.isnan(x):
|
|
469
|
+
continue
|
|
470
|
+
|
|
471
|
+
self.ax.annotate(
|
|
472
|
+
"|",
|
|
473
|
+
(x, -0.011 * self.scale),
|
|
474
|
+
xycoords=self.ax.transAxes,
|
|
475
|
+
**style.label.matplot_kwargs(self.scale / 2),
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
@cache
|
|
479
|
+
def _to_ax(self, az: float, alt: float) -> tuple[float, float]:
|
|
480
|
+
"""Converts az/alt to axes coordinates"""
|
|
481
|
+
x, y = self._proj.transform_point(az, alt, self._crs)
|
|
482
|
+
data_to_axes = self.ax.transData + self.ax.transAxes.inverted()
|
|
483
|
+
x_axes, y_axes = data_to_axes.transform((x, y))
|
|
484
|
+
return x_axes, y_axes
|
|
485
|
+
|
|
486
|
+
@cache
|
|
487
|
+
def _ax_to_azalt(self, x: float, y: float) -> tuple[float, float]:
|
|
488
|
+
trans = self.ax.transAxes + self.ax.transData.inverted()
|
|
489
|
+
x_projected, y_projected = trans.transform((x, y)) # axes to data
|
|
490
|
+
az, alt = self._crs.transform_point(x_projected, y_projected, self._proj)
|
|
491
|
+
return float(az), float(alt)
|
|
492
|
+
|
|
493
|
+
def _plot_background_clip_path(self):
|
|
494
|
+
if self.style.has_gradient_background():
|
|
495
|
+
background_color = "#ffffff00"
|
|
496
|
+
self._plot_gradient_background(self.style.background_color)
|
|
497
|
+
else:
|
|
498
|
+
background_color = self.style.background_color.as_hex()
|
|
499
|
+
|
|
500
|
+
self._background_clip_path = patches.Rectangle(
|
|
501
|
+
(0, 0),
|
|
502
|
+
width=1,
|
|
503
|
+
height=1,
|
|
504
|
+
facecolor=background_color,
|
|
505
|
+
linewidth=0,
|
|
506
|
+
fill=True,
|
|
507
|
+
zorder=-3_000,
|
|
508
|
+
transform=self.ax.transAxes,
|
|
509
|
+
)
|
|
510
|
+
self.ax.set_facecolor(background_color)
|
|
511
|
+
|
|
512
|
+
self.ax.add_patch(self._background_clip_path)
|
|
513
|
+
self._update_clip_path_polygon()
|
|
514
|
+
|
|
515
|
+
def _init_plot(self):
|
|
516
|
+
self._proj = ccrs.LambertAzimuthalEqualArea(
|
|
517
|
+
central_longitude=sum(self.az) / 2,
|
|
518
|
+
central_latitude=0,
|
|
519
|
+
)
|
|
520
|
+
self._proj.threshold = 100
|
|
521
|
+
self.fig = plt.figure(
|
|
522
|
+
figsize=(self.figure_size, self.figure_size),
|
|
523
|
+
facecolor=self.style.figure_background_color.as_hex(),
|
|
524
|
+
# layout="constrained",
|
|
525
|
+
dpi=DPI,
|
|
526
|
+
)
|
|
527
|
+
self.ax = self.fig.add_subplot(1, 1, 1, projection=self._proj)
|
|
528
|
+
self.fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
|
|
529
|
+
|
|
530
|
+
self.ax.xaxis.set_visible(False)
|
|
531
|
+
self.ax.yaxis.set_visible(False)
|
|
532
|
+
self.ax.axis("off")
|
|
533
|
+
|
|
534
|
+
bounds = [
|
|
535
|
+
self.az[0],
|
|
536
|
+
self.az[1],
|
|
537
|
+
self.alt[0],
|
|
538
|
+
self.alt[1],
|
|
539
|
+
]
|
|
540
|
+
|
|
541
|
+
self.ax.set_extent(bounds, crs=ccrs.PlateCarree())
|
|
542
|
+
self._fit_to_ax()
|
|
543
|
+
self._plot_background_clip_path()
|