starplot 0.12.5__py2.py3-none-any.whl → 0.14.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.
- starplot/__init__.py +3 -2
- starplot/base.py +408 -95
- starplot/callables.py +61 -7
- starplot/coordinates.py +6 -0
- starplot/data/bayer.py +1532 -3
- starplot/data/constellations.py +564 -2
- starplot/data/flamsteed.py +2682 -0
- starplot/data/library/constellation_borders_inv.gpkg +0 -0
- starplot/data/library/constellation_lines_hips.json +3 -1
- starplot/data/stars.py +408 -87
- starplot/geometry.py +82 -0
- starplot/horizon.py +458 -0
- starplot/map.py +97 -284
- starplot/models/base.py +9 -2
- starplot/models/constellation.py +1 -1
- starplot/optic.py +32 -14
- starplot/plotters/__init__.py +2 -0
- starplot/plotters/constellations.py +339 -0
- starplot/plotters/dsos.py +5 -1
- starplot/plotters/experimental.py +171 -0
- starplot/plotters/milkyway.py +41 -0
- starplot/plotters/stars.py +143 -13
- starplot/styles/base.py +308 -169
- starplot/styles/ext/antique.yml +54 -46
- starplot/styles/ext/blue_dark.yml +39 -45
- starplot/styles/ext/blue_light.yml +49 -30
- starplot/styles/ext/blue_medium.yml +53 -50
- starplot/styles/ext/cb_wong.yml +16 -7
- starplot/styles/ext/grayscale.yml +17 -10
- starplot/styles/ext/grayscale_dark.yml +18 -8
- starplot/styles/ext/map.yml +10 -7
- starplot/styles/ext/nord.yml +38 -38
- starplot/styles/ext/optic.yml +7 -5
- 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/hind/DESCRIPTION.en_us.html +28 -0
- starplot/styles/fonts-library/hind/Hind-Bold.ttf +0 -0
- starplot/styles/fonts-library/hind/Hind-Light.ttf +0 -0
- starplot/styles/fonts-library/hind/Hind-Medium.ttf +0 -0
- starplot/styles/fonts-library/hind/Hind-Regular.ttf +0 -0
- starplot/styles/fonts-library/hind/Hind-SemiBold.ttf +0 -0
- starplot/styles/fonts-library/hind/METADATA.pb +58 -0
- starplot/styles/fonts-library/hind/OFL.txt +93 -0
- starplot/styles/fonts-library/inter/Inter-Black.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-BlackItalic.ttf +0 -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-ExtraBoldItalic.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-Medium.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-MediumItalic.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/Inter-Thin.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-ThinItalic.ttf +0 -0
- starplot/styles/fonts-library/inter/LICENSE.txt +92 -0
- starplot/styles/fonts.py +15 -0
- starplot/styles/markers.py +207 -6
- starplot/utils.py +19 -0
- starplot/warnings.py +16 -0
- {starplot-0.12.5.dist-info → starplot-0.14.0.dist-info}/METADATA +12 -12
- starplot-0.14.0.dist-info/RECORD +107 -0
- starplot-0.12.5.dist-info/RECORD +0 -67
- {starplot-0.12.5.dist-info → starplot-0.14.0.dist-info}/LICENSE +0 -0
- {starplot-0.12.5.dist-info → starplot-0.14.0.dist-info}/WHEEL +0 -0
starplot/horizon.py
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from functools import cache
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import geopandas as gpd
|
|
6
|
+
|
|
7
|
+
from cartopy import crs as ccrs
|
|
8
|
+
from matplotlib import pyplot as plt, patches
|
|
9
|
+
from matplotlib.ticker import FixedLocator
|
|
10
|
+
from skyfield.api import wgs84, Star as SkyfieldStar
|
|
11
|
+
|
|
12
|
+
from starplot.coordinates import CoordinateSystem
|
|
13
|
+
from starplot.base import BasePlot, DPI
|
|
14
|
+
from starplot.mixins import ExtentMaskMixin
|
|
15
|
+
from starplot.plotters import (
|
|
16
|
+
ConstellationPlotterMixin,
|
|
17
|
+
StarPlotterMixin,
|
|
18
|
+
DsoPlotterMixin,
|
|
19
|
+
MilkyWayPlotterMixin,
|
|
20
|
+
)
|
|
21
|
+
from starplot.styles import (
|
|
22
|
+
PlotStyle,
|
|
23
|
+
extensions,
|
|
24
|
+
use_style,
|
|
25
|
+
PathStyle,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
pd.options.mode.chained_assignment = None # default='warn'
|
|
29
|
+
|
|
30
|
+
DEFAULT_HORIZON_STYLE = PlotStyle().extend(extensions.MAP)
|
|
31
|
+
|
|
32
|
+
DEFAULT_HORIZON_LABELS = {
|
|
33
|
+
0: "N",
|
|
34
|
+
45: "NE",
|
|
35
|
+
90: "E",
|
|
36
|
+
135: "SE",
|
|
37
|
+
180: "S",
|
|
38
|
+
225: "SW",
|
|
39
|
+
270: "W",
|
|
40
|
+
315: "NW",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class HorizonPlot(
|
|
45
|
+
BasePlot,
|
|
46
|
+
ExtentMaskMixin,
|
|
47
|
+
ConstellationPlotterMixin,
|
|
48
|
+
StarPlotterMixin,
|
|
49
|
+
DsoPlotterMixin,
|
|
50
|
+
MilkyWayPlotterMixin,
|
|
51
|
+
):
|
|
52
|
+
"""Creates a new horizon plot.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
lat: Latitude of observer's location
|
|
56
|
+
lon: Longitude of observer's location
|
|
57
|
+
altitude: Tuple of altitude range to plot (min, max)
|
|
58
|
+
azimuth: Tuple of azimuth range to plot (min, max)
|
|
59
|
+
dt: Date/time of observation (*must be timezone-aware*). Default = current UTC time.
|
|
60
|
+
ephemeris: Ephemeris to use for calculating planet positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
|
|
61
|
+
style: Styling for the plot (colors, sizes, fonts, etc)
|
|
62
|
+
resolution: Size (in pixels) of largest dimension of the map
|
|
63
|
+
hide_colliding_labels: If True, then labels will not be plotted if they collide with another existing label
|
|
64
|
+
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.
|
|
65
|
+
autoscale: If True, then the scale will be automatically set based on resolution
|
|
66
|
+
suppress_warnings: If True (the default), then all warnings will be suppressed
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
HorizonPlot: A new instance of an HorizonPlot
|
|
70
|
+
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
_coordinate_system = CoordinateSystem.AZ_ALT
|
|
74
|
+
|
|
75
|
+
FIELD_OF_VIEW_MAX = 9.0
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
lat: float,
|
|
80
|
+
lon: float,
|
|
81
|
+
altitude: tuple[float, float],
|
|
82
|
+
azimuth: tuple[float, float],
|
|
83
|
+
dt: datetime = None,
|
|
84
|
+
ephemeris: str = "de421_2001.bsp",
|
|
85
|
+
style: PlotStyle = DEFAULT_HORIZON_STYLE,
|
|
86
|
+
resolution: int = 4096,
|
|
87
|
+
hide_colliding_labels: bool = True,
|
|
88
|
+
scale: float = 1.0,
|
|
89
|
+
autoscale: bool = False,
|
|
90
|
+
suppress_warnings: bool = True,
|
|
91
|
+
*args,
|
|
92
|
+
**kwargs,
|
|
93
|
+
) -> "HorizonPlot":
|
|
94
|
+
super().__init__(
|
|
95
|
+
dt,
|
|
96
|
+
ephemeris,
|
|
97
|
+
style,
|
|
98
|
+
resolution,
|
|
99
|
+
hide_colliding_labels,
|
|
100
|
+
scale=scale,
|
|
101
|
+
autoscale=autoscale,
|
|
102
|
+
suppress_warnings=suppress_warnings,
|
|
103
|
+
*args,
|
|
104
|
+
**kwargs,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if azimuth[0] >= azimuth[1]:
|
|
108
|
+
raise ValueError("Azimuth min must be less than max")
|
|
109
|
+
if azimuth[1] - azimuth[0] > 180:
|
|
110
|
+
raise ValueError("Azimuth range cannot be greater than 180 degrees")
|
|
111
|
+
|
|
112
|
+
if altitude[0] >= altitude[1]:
|
|
113
|
+
raise ValueError("Altitude min must be less than max")
|
|
114
|
+
if altitude[1] - altitude[0] > 90:
|
|
115
|
+
raise ValueError("Altitude range cannot be greater than 90 degrees")
|
|
116
|
+
|
|
117
|
+
self.logger.debug("Creating HorizonPlot...")
|
|
118
|
+
self.alt = altitude
|
|
119
|
+
self.az = azimuth
|
|
120
|
+
self.center_alt = sum(altitude) / 2
|
|
121
|
+
self.center_az = sum(azimuth) / 2
|
|
122
|
+
self.lat = lat
|
|
123
|
+
self.lon = lon
|
|
124
|
+
|
|
125
|
+
self._geodetic = ccrs.Geodetic()
|
|
126
|
+
self._plate_carree = ccrs.PlateCarree()
|
|
127
|
+
self._crs = ccrs.CRS(
|
|
128
|
+
proj4_params=[
|
|
129
|
+
("proj", "latlong"),
|
|
130
|
+
("a", "6378137"),
|
|
131
|
+
],
|
|
132
|
+
globe=ccrs.Globe(ellipse="sphere", flattening=0),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
self._calc_position()
|
|
136
|
+
self._init_plot()
|
|
137
|
+
self._adjust_radec_minmax()
|
|
138
|
+
|
|
139
|
+
@cache
|
|
140
|
+
def _prepare_coords(self, ra, dec) -> (float, float):
|
|
141
|
+
"""Converts RA/DEC to AZ/ALT"""
|
|
142
|
+
point = SkyfieldStar(ra_hours=ra, dec_degrees=dec)
|
|
143
|
+
position = self.observe(point).apparent()
|
|
144
|
+
pos_alt, pos_az, _ = position.altaz()
|
|
145
|
+
return pos_az.degrees, pos_alt.degrees
|
|
146
|
+
|
|
147
|
+
def _prepare_star_coords(self, df):
|
|
148
|
+
stars_apparent = self.observe(SkyfieldStar.from_dataframe(df)).apparent()
|
|
149
|
+
nearby_stars_alt, nearby_stars_az, _ = stars_apparent.altaz()
|
|
150
|
+
df["x"], df["y"] = (
|
|
151
|
+
nearby_stars_az.degrees,
|
|
152
|
+
nearby_stars_alt.degrees,
|
|
153
|
+
)
|
|
154
|
+
return df
|
|
155
|
+
|
|
156
|
+
def _plot_kwargs(self) -> dict:
|
|
157
|
+
return dict(transform=self._crs)
|
|
158
|
+
|
|
159
|
+
@cache
|
|
160
|
+
def in_bounds(self, ra, dec) -> bool:
|
|
161
|
+
"""Determine if a coordinate is within the bounds of the plot.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
ra: Right ascension, in hours (0...24)
|
|
165
|
+
dec: Declination, in degrees (-90...90)
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
True if the coordinate is in bounds, otherwise False
|
|
169
|
+
"""
|
|
170
|
+
az, alt = self._prepare_coords(ra, dec)
|
|
171
|
+
return self.in_bounds_altaz(alt, az)
|
|
172
|
+
|
|
173
|
+
def in_bounds_altaz(self, alt, az, scale: float = 1) -> bool:
|
|
174
|
+
"""Determine if a coordinate is within the bounds of the plot.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
alt: Altitude angle in degrees (0...90)
|
|
178
|
+
az: Azimuth angle in degrees (0...360)
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
True if the coordinate is in bounds, otherwise False
|
|
182
|
+
"""
|
|
183
|
+
if self.az[0] > 360 or self.az[1] > 360 and az < 90:
|
|
184
|
+
az += 360
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
az < self.az[1]
|
|
188
|
+
and az > self.az[0]
|
|
189
|
+
and alt < self.alt[1]
|
|
190
|
+
and alt > self.alt[0]
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def _polygon(self, points, style, **kwargs):
|
|
194
|
+
super()._polygon(points, style, transform=self._crs, **kwargs)
|
|
195
|
+
|
|
196
|
+
def _calc_position(self):
|
|
197
|
+
earth = self.ephemeris["earth"]
|
|
198
|
+
|
|
199
|
+
self.location = earth + wgs84.latlon(self.lat, self.lon)
|
|
200
|
+
self.observe = self.location.at(self.timescale).observe
|
|
201
|
+
|
|
202
|
+
locations = [
|
|
203
|
+
self.location.at(self.timescale).from_altaz(
|
|
204
|
+
alt_degrees=self.alt[0], az_degrees=self.az[0]
|
|
205
|
+
), # lower left
|
|
206
|
+
self.location.at(self.timescale).from_altaz(
|
|
207
|
+
alt_degrees=self.alt[0], az_degrees=self.az[1]
|
|
208
|
+
), # lower right
|
|
209
|
+
self.location.at(self.timescale).from_altaz(
|
|
210
|
+
alt_degrees=self.alt[1], az_degrees=self.center_az
|
|
211
|
+
), # top center
|
|
212
|
+
self.location.at(self.timescale).from_altaz(
|
|
213
|
+
alt_degrees=self.center_alt, az_degrees=self.center_az
|
|
214
|
+
), # center
|
|
215
|
+
# self.location.at(self.timescale).from_altaz(alt_degrees=self.alt[1], az_degrees=self.az[0]), # upper left
|
|
216
|
+
# self.location.at(self.timescale).from_altaz(alt_degrees=self.alt[1], az_degrees=self.az[1]), # upper right
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
self.ra_min = None
|
|
220
|
+
self.ra_max = None
|
|
221
|
+
self.dec_max = None
|
|
222
|
+
self.dec_min = None
|
|
223
|
+
|
|
224
|
+
for location in locations:
|
|
225
|
+
ra, dec, _ = location.radec()
|
|
226
|
+
ra = ra.hours
|
|
227
|
+
dec = dec.degrees
|
|
228
|
+
if self.ra_min is None or ra < self.ra_min:
|
|
229
|
+
self.ra_min = ra
|
|
230
|
+
|
|
231
|
+
if self.ra_max is None or ra > self.ra_max:
|
|
232
|
+
self.ra_max = ra
|
|
233
|
+
|
|
234
|
+
if self.dec_min is None or dec < self.dec_min:
|
|
235
|
+
self.dec_min = dec
|
|
236
|
+
|
|
237
|
+
if self.dec_max is None or dec > self.dec_max:
|
|
238
|
+
self.dec_max = dec
|
|
239
|
+
|
|
240
|
+
def _adjust_radec_minmax(self):
|
|
241
|
+
if self.dec_max > 70 or self.dec_min < -70:
|
|
242
|
+
# naive method of getting all the stars near the poles
|
|
243
|
+
self.ra_min = 0
|
|
244
|
+
self.ra_max = 24
|
|
245
|
+
|
|
246
|
+
self.dec_min -= 20
|
|
247
|
+
self.dec_max += 20
|
|
248
|
+
self.ra_min -= 4
|
|
249
|
+
self.ra_max += 4
|
|
250
|
+
|
|
251
|
+
if self.ra_min < 0:
|
|
252
|
+
self.ra_min = 0
|
|
253
|
+
|
|
254
|
+
self.logger.debug(
|
|
255
|
+
f"Extent = RA ({self.ra_min:.2f}, {self.ra_max:.2f}) DEC ({self.dec_min:.2f}, {self.dec_max:.2f})"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
def _in_bounds_xy(self, x: float, y: float) -> bool:
|
|
259
|
+
return self.in_bounds_altaz(y, x) # alt = y, az = x
|
|
260
|
+
|
|
261
|
+
def _read_geo_package(self, filename: str):
|
|
262
|
+
"""Returns GeoDataFrame of a GeoPackage file"""
|
|
263
|
+
|
|
264
|
+
# if self.ra_min <= 0 and self.ra_max >= 24:
|
|
265
|
+
# lon_min = -180
|
|
266
|
+
# lon_max = 180
|
|
267
|
+
# else:
|
|
268
|
+
# lon_min = self.ra_max * 15 - 180 # ra_to_lon(24 - self.ra_max)
|
|
269
|
+
# lon_max = self.ra_min * 15 - 180 # ra_to_lon(24 - self.ra_min)
|
|
270
|
+
|
|
271
|
+
# extent = self._extent_mask()
|
|
272
|
+
# extent = (
|
|
273
|
+
# lon_min,
|
|
274
|
+
# self.dec_min,
|
|
275
|
+
# lon_max,
|
|
276
|
+
# self.dec_max,
|
|
277
|
+
# )
|
|
278
|
+
|
|
279
|
+
return gpd.read_file(
|
|
280
|
+
filename,
|
|
281
|
+
engine="pyogrio",
|
|
282
|
+
use_arrow=True,
|
|
283
|
+
# bbox=extent,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
@use_style(PathStyle, "horizon")
|
|
287
|
+
def horizon(
|
|
288
|
+
self,
|
|
289
|
+
style: PathStyle = None,
|
|
290
|
+
labels: dict[int, str] = DEFAULT_HORIZON_LABELS,
|
|
291
|
+
show_degree_labels: bool = True,
|
|
292
|
+
degree_step: int = 15,
|
|
293
|
+
show_ticks: bool = True,
|
|
294
|
+
tick_step: int = 5,
|
|
295
|
+
):
|
|
296
|
+
"""
|
|
297
|
+
Plots rectangle for horizon that shows cardinal directions and azimuth labels.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
style: Style of the horizon path. If None, then the plot's style definition will be used.
|
|
301
|
+
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)
|
|
302
|
+
show_degree_labels: If True, then azimuth degree labels will be plotted on the horizon path
|
|
303
|
+
degree_step: Step size for degree labels
|
|
304
|
+
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
|
|
305
|
+
tick_step: Step size for tick marks
|
|
306
|
+
"""
|
|
307
|
+
bottom = patches.Polygon(
|
|
308
|
+
[
|
|
309
|
+
(0, 0),
|
|
310
|
+
(1, 0),
|
|
311
|
+
(1, -0.1 * self.scale),
|
|
312
|
+
(0, -0.1 * self.scale),
|
|
313
|
+
(0, 0),
|
|
314
|
+
],
|
|
315
|
+
color=style.line.color.as_hex(),
|
|
316
|
+
transform=self.ax.transAxes,
|
|
317
|
+
clip_on=False,
|
|
318
|
+
)
|
|
319
|
+
self.ax.add_patch(bottom)
|
|
320
|
+
|
|
321
|
+
def az_to_ax(d):
|
|
322
|
+
return self._to_ax(d, self.alt[0])[0]
|
|
323
|
+
|
|
324
|
+
for az in range(self.az[0] + 2, self.az[1], 1):
|
|
325
|
+
az = int(az)
|
|
326
|
+
|
|
327
|
+
if az >= 360:
|
|
328
|
+
az -= 360
|
|
329
|
+
|
|
330
|
+
if labels.get(az):
|
|
331
|
+
self.ax.annotate(
|
|
332
|
+
labels.get(az),
|
|
333
|
+
(az_to_ax(az), -0.074 * self.scale),
|
|
334
|
+
xycoords=self.ax.transAxes,
|
|
335
|
+
**style.label.matplot_kwargs(self.scale),
|
|
336
|
+
clip_on=False,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
if show_degree_labels and az % degree_step == 0:
|
|
340
|
+
self.ax.annotate(
|
|
341
|
+
str(az) + "\u00b0",
|
|
342
|
+
(az_to_ax(az), -0.011 * self.scale),
|
|
343
|
+
xycoords=self.ax.transAxes,
|
|
344
|
+
**self.style.gridlines.label.matplot_kwargs(self.scale),
|
|
345
|
+
clip_on=False,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
elif show_ticks and az % tick_step == 0:
|
|
349
|
+
self.ax.annotate(
|
|
350
|
+
"|",
|
|
351
|
+
(az_to_ax(az), -0.011 * self.scale),
|
|
352
|
+
xycoords=self.ax.transAxes,
|
|
353
|
+
**self.style.gridlines.label.matplot_kwargs(self.scale / 2),
|
|
354
|
+
clip_on=False,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
self.ax.plot(
|
|
358
|
+
[0, 1],
|
|
359
|
+
[-0.04 * self.scale, -0.04 * self.scale],
|
|
360
|
+
lw=1,
|
|
361
|
+
color=style.label.font_color.as_hex(),
|
|
362
|
+
clip_on=False,
|
|
363
|
+
transform=self.ax.transAxes,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
@use_style(PathStyle, "gridlines")
|
|
367
|
+
def gridlines(
|
|
368
|
+
self,
|
|
369
|
+
style: PathStyle = None,
|
|
370
|
+
az_locations: list[float] = None,
|
|
371
|
+
alt_locations: list[float] = None,
|
|
372
|
+
):
|
|
373
|
+
"""
|
|
374
|
+
Plots gridlines
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
style: Styling of the gridlines. If None, then the plot's style (specified when creating the plot) will be used
|
|
378
|
+
az_locations: List of azimuth locations for the gridlines (in degrees, 0...360). Defaults to every 15 degrees
|
|
379
|
+
alt_locations: List of altitude locations for the gridlines (in degrees, -90...90). Defaults to every 10 degrees.
|
|
380
|
+
|
|
381
|
+
"""
|
|
382
|
+
x_locations = az_locations or [x for x in range(0, 360, 15)]
|
|
383
|
+
x_locations = [x - 180 for x in x_locations]
|
|
384
|
+
y_locations = alt_locations or [d for d in range(-90, 90, 10)]
|
|
385
|
+
|
|
386
|
+
line_style_kwargs = style.line.matplot_kwargs()
|
|
387
|
+
gridlines = self.ax.gridlines(
|
|
388
|
+
draw_labels=False,
|
|
389
|
+
x_inline=False,
|
|
390
|
+
y_inline=False,
|
|
391
|
+
rotate_labels=False,
|
|
392
|
+
xpadding=12,
|
|
393
|
+
ypadding=12,
|
|
394
|
+
clip_on=True,
|
|
395
|
+
clip_path=self._background_clip_path,
|
|
396
|
+
gid="gridlines",
|
|
397
|
+
**line_style_kwargs,
|
|
398
|
+
)
|
|
399
|
+
gridlines.xlocator = FixedLocator(x_locations)
|
|
400
|
+
gridlines.ylocator = FixedLocator(y_locations)
|
|
401
|
+
|
|
402
|
+
@cache
|
|
403
|
+
def _to_ax(self, az: float, alt: float) -> tuple[float, float]:
|
|
404
|
+
"""Converts az/alt to axes coordinates"""
|
|
405
|
+
x, y = self._proj.transform_point(az, alt, self._crs)
|
|
406
|
+
data_to_axes = self.ax.transData + self.ax.transAxes.inverted()
|
|
407
|
+
x_axes, y_axes = data_to_axes.transform((x, y))
|
|
408
|
+
return x_axes, y_axes
|
|
409
|
+
|
|
410
|
+
def _fit_to_ax(self) -> None:
|
|
411
|
+
bbox = self.ax.get_window_extent().transformed(
|
|
412
|
+
self.fig.dpi_scale_trans.inverted()
|
|
413
|
+
)
|
|
414
|
+
width, height = bbox.width, bbox.height
|
|
415
|
+
self.fig.set_size_inches(width, height)
|
|
416
|
+
|
|
417
|
+
def _plot_background_clip_path(self):
|
|
418
|
+
self._background_clip_path = patches.Rectangle(
|
|
419
|
+
(0, 0),
|
|
420
|
+
width=1,
|
|
421
|
+
height=1,
|
|
422
|
+
facecolor=self.style.background_color.as_hex(),
|
|
423
|
+
linewidth=0,
|
|
424
|
+
fill=True,
|
|
425
|
+
zorder=-3_000,
|
|
426
|
+
transform=self.ax.transAxes,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
self.ax.add_patch(self._background_clip_path)
|
|
430
|
+
|
|
431
|
+
def _init_plot(self):
|
|
432
|
+
self._proj = ccrs.LambertAzimuthalEqualArea(
|
|
433
|
+
central_longitude=sum(self.az) / 2,
|
|
434
|
+
central_latitude=0,
|
|
435
|
+
)
|
|
436
|
+
self._proj.threshold = 100
|
|
437
|
+
self.fig = plt.figure(
|
|
438
|
+
figsize=(self.figure_size, self.figure_size),
|
|
439
|
+
facecolor=self.style.figure_background_color.as_hex(),
|
|
440
|
+
layout="constrained",
|
|
441
|
+
dpi=DPI,
|
|
442
|
+
)
|
|
443
|
+
self.ax = plt.axes(projection=self._proj)
|
|
444
|
+
self.ax.xaxis.set_visible(False)
|
|
445
|
+
self.ax.yaxis.set_visible(False)
|
|
446
|
+
self.ax.axis("off")
|
|
447
|
+
|
|
448
|
+
bounds = [
|
|
449
|
+
self.az[0],
|
|
450
|
+
self.az[1],
|
|
451
|
+
self.alt[0],
|
|
452
|
+
self.alt[1],
|
|
453
|
+
]
|
|
454
|
+
|
|
455
|
+
self.ax.set_extent(bounds, crs=ccrs.PlateCarree())
|
|
456
|
+
|
|
457
|
+
self._plot_background_clip_path()
|
|
458
|
+
self._fit_to_ax()
|