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