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.
Files changed (98) hide show
  1. starplot/__init__.py +59 -0
  2. starplot/callables.py +176 -0
  3. starplot/cli.py +29 -0
  4. starplot/config.py +80 -0
  5. starplot/coordinates.py +7 -0
  6. starplot/data/__init__.py +22 -0
  7. starplot/data/catalogs.py +356 -0
  8. starplot/data/constellations.py +71 -0
  9. starplot/data/db.py +33 -0
  10. starplot/data/dsos.py +85 -0
  11. starplot/data/ecliptic.py +201 -0
  12. starplot/data/library/constellation_names.parquet +0 -0
  13. starplot/data/library/dso_names.parquet +0 -0
  14. starplot/data/library/readme.md +1 -0
  15. starplot/data/library/star_designations.parquet +0 -0
  16. starplot/data/stars.py +79 -0
  17. starplot/data/translations.py +408 -0
  18. starplot/data/utils.py +35 -0
  19. starplot/geometry.py +319 -0
  20. starplot/mixins.py +360 -0
  21. starplot/models/__init__.py +21 -0
  22. starplot/models/base.py +77 -0
  23. starplot/models/comet.py +302 -0
  24. starplot/models/constellation.py +151 -0
  25. starplot/models/dso.py +317 -0
  26. starplot/models/milky_way.py +30 -0
  27. starplot/models/moon.py +130 -0
  28. starplot/models/objects.py +29 -0
  29. starplot/models/observer.py +125 -0
  30. starplot/models/optics.py +342 -0
  31. starplot/models/planet.py +137 -0
  32. starplot/models/satellite.py +138 -0
  33. starplot/models/star.py +253 -0
  34. starplot/models/sun.py +62 -0
  35. starplot/plots/__init__.py +7 -0
  36. starplot/plots/base.py +1023 -0
  37. starplot/plots/galaxy.py +372 -0
  38. starplot/plots/horizon.py +543 -0
  39. starplot/plots/map.py +515 -0
  40. starplot/plots/optic.py +469 -0
  41. starplot/plots/zenith.py +217 -0
  42. starplot/plotters/__init__.py +9 -0
  43. starplot/plotters/arrow.py +174 -0
  44. starplot/plotters/constellations.py +298 -0
  45. starplot/plotters/debug.py +21 -0
  46. starplot/plotters/dsos.py +294 -0
  47. starplot/plotters/experimental.py +722 -0
  48. starplot/plotters/gradients.py +153 -0
  49. starplot/plotters/legend.py +253 -0
  50. starplot/plotters/milkyway.py +51 -0
  51. starplot/plotters/stars.py +319 -0
  52. starplot/plotters/text.py +802 -0
  53. starplot/profile.py +16 -0
  54. starplot/projections.py +184 -0
  55. starplot/styles/__init__.py +6 -0
  56. starplot/styles/base.py +1344 -0
  57. starplot/styles/ext/antique.yml +175 -0
  58. starplot/styles/ext/blue_dark.yml +163 -0
  59. starplot/styles/ext/blue_gold.yml +147 -0
  60. starplot/styles/ext/blue_light.yml +123 -0
  61. starplot/styles/ext/blue_medium.yml +142 -0
  62. starplot/styles/ext/blue_night.yml +185 -0
  63. starplot/styles/ext/cb_wong.yml +124 -0
  64. starplot/styles/ext/color_print.yml +111 -0
  65. starplot/styles/ext/gradient_presets.yml +158 -0
  66. starplot/styles/ext/grayscale.yml +94 -0
  67. starplot/styles/ext/grayscale_dark.yml +136 -0
  68. starplot/styles/ext/map.yml +12 -0
  69. starplot/styles/ext/nord.yml +158 -0
  70. starplot/styles/ext/optic.yml +20 -0
  71. starplot/styles/ext/publication.yml +8 -0
  72. starplot/styles/extensions.py +129 -0
  73. starplot/styles/fonts-library/gfs-didot/DESCRIPTION.en_us.html +9 -0
  74. starplot/styles/fonts-library/gfs-didot/GFSDidot-Regular.ttf +0 -0
  75. starplot/styles/fonts-library/gfs-didot/METADATA.pb +16 -0
  76. starplot/styles/fonts-library/gfs-didot/OFL.txt +94 -0
  77. starplot/styles/fonts-library/inter/Inter-Bold.ttf +0 -0
  78. starplot/styles/fonts-library/inter/Inter-BoldItalic.ttf +0 -0
  79. starplot/styles/fonts-library/inter/Inter-ExtraBold.ttf +0 -0
  80. starplot/styles/fonts-library/inter/Inter-ExtraLight.ttf +0 -0
  81. starplot/styles/fonts-library/inter/Inter-ExtraLightItalic.ttf +0 -0
  82. starplot/styles/fonts-library/inter/Inter-Italic.ttf +0 -0
  83. starplot/styles/fonts-library/inter/Inter-Light.ttf +0 -0
  84. starplot/styles/fonts-library/inter/Inter-LightItalic.ttf +0 -0
  85. starplot/styles/fonts-library/inter/Inter-Regular.ttf +0 -0
  86. starplot/styles/fonts-library/inter/Inter-SemiBold.ttf +0 -0
  87. starplot/styles/fonts-library/inter/Inter-SemiBoldItalic.ttf +0 -0
  88. starplot/styles/fonts-library/inter/LICENSE.txt +92 -0
  89. starplot/styles/fonts.py +15 -0
  90. starplot/styles/helpers.py +93 -0
  91. starplot/styles/markers.py +308 -0
  92. starplot/utils.py +169 -0
  93. starplot/warnings.py +21 -0
  94. starplot-0.20.5.dist-info/METADATA +146 -0
  95. starplot-0.20.5.dist-info/RECORD +98 -0
  96. starplot-0.20.5.dist-info/WHEEL +4 -0
  97. starplot-0.20.5.dist-info/entry_points.txt +3 -0
  98. starplot-0.20.5.dist-info/licenses/LICENSE +21 -0
starplot/data/utils.py ADDED
@@ -0,0 +1,35 @@
1
+ import sys
2
+
3
+ import requests
4
+ import numpy as np
5
+
6
+
7
+ def download(url, download_path, description: str = "", silent=False):
8
+ with open(download_path, "wb") as f:
9
+ if not silent:
10
+ print(f"Downloading {description}...")
11
+
12
+ response = requests.get(url, stream=True)
13
+ total_size = response.headers.get("content-length")
14
+
15
+ if total_size is None:
16
+ f.write(response.content)
17
+ return
18
+
19
+ bytes_written = 0
20
+ total_size = int(total_size)
21
+ for chunk in response.iter_content(chunk_size=4096):
22
+ bytes_written += len(chunk)
23
+ f.write(chunk)
24
+
25
+ progress = int(25 * bytes_written / total_size)
26
+ if not silent:
27
+ sys.stdout.write("\r[%s%s]" % ("=" * progress, " " * (25 - progress)))
28
+ sys.stdout.flush()
29
+
30
+ if not silent:
31
+ print("Download complete!")
32
+
33
+
34
+ def to_pandas(value):
35
+ return value.to_pandas().replace({np.nan: None})
starplot/geometry.py ADDED
@@ -0,0 +1,319 @@
1
+ import random
2
+ import math
3
+
4
+ import pyproj
5
+ import numpy as np
6
+
7
+ from shapely import union_all
8
+ from shapely.geometry import Point, Polygon, LineString
9
+
10
+ GLOBAL_EXTENT = Polygon(
11
+ [
12
+ [0, -90],
13
+ [360, -90],
14
+ [360, 90],
15
+ [0, 90],
16
+ [0, -90],
17
+ ]
18
+ )
19
+
20
+ GEOD = pyproj.Geod("+a=6378137 +f=0.0", sphere=True)
21
+
22
+
23
+ def distance_m(distance_degrees: float, lat: float = 0, lon: float = 0):
24
+ _, _, distance = GEOD.inv(lon, lat, lon + distance_degrees, lat)
25
+ return distance
26
+
27
+
28
+ def away_from_poles(dec):
29
+ # for some reason cartopy does not like plotting things EXACTLY at the poles
30
+ # so, this is a little hack to avoid the bug (or maybe a misconception?) by
31
+ # plotting a tiny bit away from the pole
32
+ if dec == 90:
33
+ dec -= 0.00000001
34
+ if dec == -90:
35
+ dec += 0.00000001
36
+
37
+ return dec
38
+
39
+
40
+ def rectangle(
41
+ center: tuple,
42
+ height_degrees: float,
43
+ width_degrees: float,
44
+ angle: float = 0,
45
+ ) -> Polygon:
46
+ """
47
+ Returns a rectangle polygon on a sphere, with coordinates in degrees.
48
+
49
+ If the rectangle crosses the meridian at X=0, then the X coordinates will extend past 360.
50
+
51
+ Args:
52
+ center: Center of rectangle (x, y) in degrees
53
+ height_degrees: Height of rectangle in degrees
54
+ width_degrees: Width of rectangle in degrees
55
+ angle: Angle to rotate rectangle, in degrees
56
+
57
+ Returns:
58
+ Polygon of rectangle
59
+ """
60
+
61
+ ra, dec = center
62
+ dec = away_from_poles(dec)
63
+ angle = 180 - angle
64
+
65
+ height_m = distance_m(height_degrees)
66
+ width_m = distance_m(width_degrees)
67
+
68
+ distance = math.sqrt((height_m / 2) ** 2 + (width_m / 2) ** 2)
69
+ angle_th = math.atan((height_m / 2) / (width_m / 2))
70
+
71
+ angle_th = math.degrees(angle_th)
72
+ points = []
73
+
74
+ lons, lats, _ = GEOD.fwd(
75
+ [ra] * 4,
76
+ [dec] * 4,
77
+ [
78
+ angle + (90 - angle_th),
79
+ angle + (90 + angle_th),
80
+ angle + (270 - angle_th),
81
+ angle + (270 + angle_th),
82
+ ],
83
+ [distance] * 4,
84
+ )
85
+ if min(lons) < 0:
86
+ lons = [lon + 360 for lon in lons]
87
+
88
+ points = list(zip(lons, lats))
89
+ points = [(round(ra, 4), round(dec, 4)) for ra, dec in points]
90
+ points.append(points[0])
91
+ return Polygon(points)
92
+
93
+
94
+ def ellipse(
95
+ center: tuple,
96
+ height_degrees: float,
97
+ width_degrees: float,
98
+ angle: float = 0,
99
+ num_pts: int = 100,
100
+ start_angle: int = 0,
101
+ end_angle: int = 360,
102
+ ) -> Polygon:
103
+ """
104
+ Returns an ellipse polygon on a sphere, with coordinates in degrees.
105
+
106
+ If the ellipse crosses the meridian at X=0, then the X coordinates will extend past 360.
107
+
108
+ Args:
109
+ center: Center of ellipse (x, y) in degrees
110
+ height_degrees: Height of ellipse in degrees
111
+ width_degrees: Width of ellipse in degrees
112
+ angle: Angle to rotate ellipse, in degrees
113
+ num_pts: Number of evenly-spaced points to generate for the ellipse. At least 100 is recommended to ensure good-looking curves.
114
+ start_angle: Angle to start drawing the ellipse
115
+ end_angle: Angle to stop drawing the ellipse
116
+
117
+ Returns:
118
+ Polygon of ellipse
119
+ """
120
+
121
+ ra, dec = center
122
+ dec = away_from_poles(dec)
123
+ angle = 180 - angle
124
+
125
+ height = distance_m(height_degrees / 2) # b
126
+ width = distance_m(width_degrees / 2) # a
127
+ step_size = (end_angle - start_angle) / num_pts
128
+
129
+ lons = []
130
+ lats = []
131
+ points = []
132
+ for angle_pt in np.arange(start_angle, end_angle + step_size, step_size):
133
+ radians = math.radians(angle_pt)
134
+ radius_a = (height * width) / math.sqrt(
135
+ height**2 * (math.sin(radians)) ** 2
136
+ + width**2 * (math.cos(radians)) ** 2
137
+ )
138
+ lon, lat, _ = GEOD.fwd([ra], [dec], angle + angle_pt, radius_a)
139
+
140
+ lons.append(lon[0])
141
+ lats.append(lat[0])
142
+
143
+ if min(lons) < 0:
144
+ lons = [lon + 360 for lon in lons]
145
+
146
+ points = list(zip(lons, lats))
147
+ points = [(round(ra, 4), round(dec, 4)) for ra, dec in points]
148
+ points.append(points[0])
149
+ return Polygon(points)
150
+
151
+
152
+ def circle(center, diameter_degrees, num_pts=100) -> Polygon:
153
+ return ellipse(
154
+ center,
155
+ diameter_degrees,
156
+ diameter_degrees,
157
+ angle=0,
158
+ num_pts=num_pts,
159
+ )
160
+
161
+
162
+ def union_at_zero(a: Polygon, b: Polygon) -> Polygon:
163
+ """
164
+ Returns union of two polygons on a sphere, with coordinates in degrees.
165
+
166
+ If the two polygons share a border at the X=0 meridian, then the returned union will have X coordiantes that extend past 360 degrees.
167
+
168
+ Args:
169
+ a: First polygon
170
+ b: Second polygon
171
+
172
+ Returns
173
+ Polygon union of first and second polygon
174
+ """
175
+ a_ra = list(a.exterior.coords.xy)[0]
176
+ b_ra = list(b.exterior.coords.xy)[0]
177
+
178
+ if max(a_ra) == 360 and min(b_ra) == 0:
179
+ points = list(zip(*b.exterior.coords.xy))
180
+ b = Polygon([[ra + 360, dec] for ra, dec in points])
181
+
182
+ elif min(a_ra) == 0 and max(b_ra) == 360:
183
+ points = list(zip(*a.exterior.coords.xy))
184
+ a = Polygon([[ra + 360, dec] for ra, dec in points])
185
+
186
+ return union_all([a, b])
187
+
188
+
189
+ def split_polygon_at_zero(polygon: Polygon) -> list[Polygon]:
190
+ """
191
+ Splits a polygon at the first point of Aries (RA=0)
192
+
193
+ Args:
194
+ polygon: Polygon that possibly needs splitting
195
+
196
+ Returns:
197
+ List of polygons
198
+ """
199
+ ra, dec = [p for p in polygon.exterior.coords.xy]
200
+
201
+ if min(ra) < 180 and max(ra) > 300:
202
+ new_ra = [r + 360 if r < 180 else r for r in ra]
203
+ new_polygon = Polygon(list(zip(new_ra, dec)))
204
+
205
+ polygon_1 = new_polygon.intersection(
206
+ Polygon(
207
+ [
208
+ [0, -90],
209
+ [360, -90],
210
+ [360, 90],
211
+ [0, 90],
212
+ [0, -90],
213
+ ]
214
+ )
215
+ )
216
+
217
+ polygon_2 = new_polygon.intersection(
218
+ Polygon(
219
+ [
220
+ [360, -90],
221
+ [720, -90],
222
+ [720, 90],
223
+ [360, 90],
224
+ [360, -90],
225
+ ]
226
+ )
227
+ )
228
+
229
+ p2_ra, p2_dec = [p for p in polygon_2.exterior.coords.xy]
230
+ p2_new_ra = [ra - 360 for ra in p2_ra]
231
+
232
+ return [polygon_1, Polygon(list(zip(p2_new_ra, p2_dec)))]
233
+
234
+ return [polygon]
235
+
236
+
237
+ def split_line_at_meridian(p1, p2, meridian=360):
238
+ """Split a line that crosses the meridian into two segments."""
239
+ x1, y1 = p1
240
+ x2, y2 = p2
241
+
242
+ # Interpolate the crossing point
243
+ t = (meridian - x1) / (x2 - x1)
244
+ y_cross = y1 + t * (y2 - y1)
245
+
246
+ # Two segments on either side
247
+ seg1 = [(x1, y1), (359.9999999, y_cross)]
248
+ seg2 = [(0, y_cross), (x2 - 360, y2)] # or -meridian depending on convention
249
+
250
+ return seg1, seg2
251
+
252
+
253
+ def random_point_in_polygon_at_distance(
254
+ polygon: Polygon,
255
+ origin_point: Point,
256
+ distance: int,
257
+ max_iterations: int = 100,
258
+ seed: int = None,
259
+ ) -> Point:
260
+ """Returns a random point inside a polygon, at a specified distance from the origin point"""
261
+ if seed:
262
+ random.seed(seed)
263
+
264
+ ctr = 0
265
+ while ctr < max_iterations:
266
+ ctr += 1
267
+ angle = random.uniform(0, 2 * math.pi)
268
+ x = origin_point.x + distance * math.cos(angle)
269
+ y = origin_point.y + distance * math.sin(angle)
270
+ point = Point(x, y)
271
+
272
+ if polygon.contains(point):
273
+ return point
274
+
275
+ return None
276
+
277
+
278
+ def is_wrapped_polygon(polygon: Polygon) -> bool:
279
+ if "MultiPolygon" == str(polygon.geom_type):
280
+ return False
281
+
282
+ ra, _ = [p for p in polygon.exterior.coords.xy]
283
+
284
+ if min(ra) < 180 and max(ra) > 300:
285
+ return True
286
+
287
+ return False
288
+
289
+
290
+ def line_segment(start, end, step) -> list[tuple[float, float]]:
291
+ """Returns coordinates on the line from start to end at the specified step-size"""
292
+ return LineString([start, end]).segmentize(step).coords
293
+
294
+
295
+ class BaseGeometry:
296
+
297
+ """
298
+ Wrapper around shapely geometries
299
+
300
+ Two types of polygons needed:
301
+ 1. For intersection testing: needs to be split at zero and restricted to 0-360
302
+ 2. For plotting: needs to be extended past 360 if applicable
303
+
304
+ TODO:
305
+
306
+ Functions
307
+ - intersects
308
+
309
+ Properties
310
+ - centroid
311
+ - bbox
312
+ - wkt
313
+ - wkb
314
+
315
+ """
316
+
317
+ def intersects(self):
318
+ """ """
319
+ pass
starplot/mixins.py ADDED
@@ -0,0 +1,360 @@
1
+ from functools import cache
2
+
3
+ from shapely import Polygon, MultiPolygon
4
+ from starplot.profile import profile
5
+
6
+
7
+ class ExtentMaskMixin:
8
+ @cache
9
+ def _extent_mask(self):
10
+ """
11
+ Returns shapely geometry objects of extent (RA = 0...360)
12
+
13
+ If the extent crosses equinox, then a MultiPolygon will be returned
14
+ """
15
+ if self._is_global_extent():
16
+ return Polygon(
17
+ [
18
+ (0, -90),
19
+ (360, -90),
20
+ (360, 90),
21
+ (0, 90),
22
+ (0, -90),
23
+ ]
24
+ )
25
+
26
+ ra_min = self.ra_min
27
+ ra_max = self.ra_max
28
+ dec_min = self.dec_min
29
+ dec_max = self.dec_max
30
+
31
+ if ra_max <= 360:
32
+ coords = [
33
+ [ra_min, dec_min],
34
+ [ra_max, dec_min],
35
+ [ra_max, dec_max],
36
+ [ra_min, dec_max],
37
+ [ra_min, dec_min],
38
+ ]
39
+ return Polygon(coords)
40
+
41
+ else:
42
+ coords_1 = [
43
+ [ra_min, dec_min],
44
+ [360, dec_min],
45
+ [360, dec_max],
46
+ [ra_min, dec_max],
47
+ [ra_min, dec_min],
48
+ ]
49
+ coords_2 = [
50
+ [0, dec_min],
51
+ [(ra_max - 360), dec_min],
52
+ [(ra_max - 360), dec_max],
53
+ [0, dec_max],
54
+ [0, dec_min],
55
+ ]
56
+
57
+ return MultiPolygon(
58
+ [
59
+ Polygon(coords_1),
60
+ Polygon(coords_2),
61
+ ]
62
+ )
63
+
64
+ def _is_global_extent(self):
65
+ """Returns True if the plot's RA/DEC range is the entire celestial sphere"""
66
+ return all(
67
+ [
68
+ self.ra_min == 0,
69
+ self.ra_max == 360,
70
+ self.dec_min == -90,
71
+ self.dec_max == 90,
72
+ ]
73
+ )
74
+
75
+
76
+ class HorizonExtentMaskMixin:
77
+ """Experimental"""
78
+
79
+ @cache
80
+ def _extent_mask_original(self):
81
+ """
82
+ Returns shapely geometry objects of extent (RA = 0...360)
83
+
84
+ If the extent crosses equinox, then a MultiPolygon will be returned
85
+ """
86
+ if self.ra_max <= 360:
87
+ coords = [
88
+ [self.ra_min, self.dec_min],
89
+ [self.ra_max, self.dec_min],
90
+ [self.ra_max, self.dec_max],
91
+ [self.ra_min, self.dec_max],
92
+ [self.ra_min, self.dec_min],
93
+ ]
94
+ return Polygon(coords)
95
+
96
+ else:
97
+ coords_1 = [
98
+ [self.ra_min, self.dec_min],
99
+ [360, self.dec_min],
100
+ [360, self.dec_max],
101
+ [self.ra_min, self.dec_max],
102
+ [self.ra_min, self.dec_min],
103
+ ]
104
+ coords_2 = [
105
+ [0, self.dec_min],
106
+ [(self.ra_max - 360), self.dec_min],
107
+ [(self.ra_max - 360), self.dec_max],
108
+ [0, self.dec_max],
109
+ [0, self.dec_min],
110
+ ]
111
+
112
+ return MultiPolygon(
113
+ [
114
+ Polygon(coords_1),
115
+ Polygon(coords_2),
116
+ ]
117
+ )
118
+
119
+ @profile
120
+ @cache
121
+ def _extent_mask2(self):
122
+ """generally working"""
123
+ coords = []
124
+
125
+ alt0, alt1 = self._alt
126
+ az0, az1 = self._az
127
+
128
+ for alt in range(0, 90, 5):
129
+ for az in range(az0 - 30, az1 + 30, 5):
130
+ ra, dec, _ = (
131
+ self.location.at(self.observer.timescale)
132
+ .from_altaz(alt_degrees=alt, az_degrees=az)
133
+ .radec()
134
+ )
135
+ ra = ra.hours * 15
136
+ if ra < 180:
137
+ ra += 360
138
+ dec = dec.degrees
139
+ coords.append([float(ra), float(dec)])
140
+
141
+ extent = Polygon(coords)
142
+ from shapely import segmentize
143
+
144
+ extent = segmentize(extent, max_segment_length=1)
145
+
146
+ self.polygon(style__fill_color="red", style__alpha=0.3, geometry=extent)
147
+ # print(extent)
148
+
149
+ return extent
150
+
151
+ @profile
152
+ @cache
153
+ def _extent_mask1(self):
154
+ from shapely import segmentize
155
+
156
+ mask_altaz = self._extent_mask_altaz()
157
+
158
+ altaz_polygons = []
159
+
160
+ if str(mask_altaz.geom_type) == "MultiPolygon":
161
+ altaz_polygons = [
162
+ segmentize(p, max_segment_length=5) for p in mask_altaz.geoms
163
+ ]
164
+ else:
165
+ altaz_polygons = [segmentize(mask_altaz, max_segment_length=5)]
166
+
167
+ radec_polygons = []
168
+
169
+ for p in altaz_polygons:
170
+ coords = []
171
+
172
+ for az, alt in list(zip(*p.exterior.coords.xy)):
173
+ ra, dec, _ = (
174
+ self.location.at(self.observer.timescale)
175
+ .from_altaz(alt_degrees=alt, az_degrees=az)
176
+ .radec()
177
+ )
178
+ ra = ra.hours * 15
179
+ if ra < 180:
180
+ ra += 360
181
+ dec = dec.degrees
182
+ coords.append([float(ra), float(dec)])
183
+
184
+ radec_polygons.append(Polygon(coords))
185
+
186
+ mp = MultiPolygon(radec_polygons)
187
+ extent = mp.convex_hull
188
+
189
+ # extent = segmentize(extent, max_segment_length=1)
190
+
191
+ print(extent.area)
192
+
193
+ # self.polygon(
194
+ # style__fill_color="red",
195
+ # style__alpha=0.3,
196
+ # geometry=extent
197
+ # )
198
+ # print(extent)
199
+
200
+ return extent
201
+
202
+ def _axes_to_azalt(self, x: float, y: float) -> tuple[float, float]:
203
+ trans = self.ax.transAxes + self.ax.transData.inverted()
204
+ x_projected, y_projected = self.ax.transAxes.inverted().transform((x, y))
205
+ x_projected, y_projected = trans.transform((x, y)) # axes to data
206
+ az, alt = self._crs.transform_point(x_projected, y_projected, self._proj)
207
+ return float(az), float(alt)
208
+
209
+ @profile
210
+ @cache
211
+ def _extent_mask(self):
212
+ coords = []
213
+ azalt = []
214
+
215
+ az0, az1 = int(self.az[0]), int(self.az[1])
216
+ alt0, alt1 = int(self.alt[0]), int(self.alt[1])
217
+
218
+ for az in range(az0, az1, 5):
219
+ for alt in range(alt0, alt1, 5):
220
+ # ax = x / 100
221
+ # ay = y / 100
222
+
223
+ # az, alt = self._axes_to_azalt(ax, ay)
224
+
225
+ if az > 360:
226
+ az -= 360
227
+
228
+ azalt.append([az, alt])
229
+ ra, dec, _ = (
230
+ self.location.at(self.observer.timescale)
231
+ .from_altaz(alt_degrees=alt, az_degrees=az)
232
+ .radec()
233
+ )
234
+ ra = float(ra.hours * 15)
235
+ dec = float(dec.degrees)
236
+
237
+ if dec > 75:
238
+ dec = 90
239
+
240
+ if ra < 180:
241
+ ra += 360
242
+ coords.append([float(ra), float(dec)])
243
+
244
+ # if prev_ra is not None and abs(ra - prev_ra) > 180:
245
+ # current_polygon_coords += [current_polygon_coords[0]]
246
+ # polygon_coords.append(
247
+ # current_polygon_coords
248
+ # )
249
+ # current_polygon_coords = [[ra, dec]]
250
+ # else:
251
+ # current_polygon_coords.append([ra, dec])
252
+
253
+ # prev_ra = ra
254
+
255
+ # if current_polygon_coords:
256
+ # current_polygon_coords += [current_polygon_coords[0]]
257
+ # polygon_coords.append(current_polygon_coords)
258
+ # print(current_polygon_coords)
259
+
260
+ # print(len(polygon_coords))
261
+ from starplot.geometry import split_polygon_at_zero
262
+
263
+ # polygons = split_polygon_at_zero(extent)
264
+
265
+ from shapely import convex_hull, MultiPoint
266
+ from pprint import pprint
267
+
268
+ # extent = MultiPolygon([Polygon(c) for c in polygon_coords])
269
+ # extent = Polygon(coords)
270
+ extent = convex_hull(MultiPoint(coords))
271
+ polygons = split_polygon_at_zero(extent)
272
+
273
+ pprint(polygons)
274
+
275
+ # extent = extent.convex_hull
276
+ # from shapely import segmentize
277
+ # polygons = [segmentize(p, max_segment_length=1) for p in polygons]
278
+ mpoly = MultiPolygon(polygons)
279
+
280
+ for p in mpoly.geoms:
281
+ self.polygon(style__fill_color="red", style__alpha=0.3, geometry=p)
282
+
283
+ # print(extent)
284
+
285
+ return mpoly
286
+
287
+ def _is_global_extent(self):
288
+ """Returns True if the plot's RA/DEC range is the entire celestial sphere"""
289
+ return all(
290
+ [
291
+ self.ra_min == 0,
292
+ self.ra_max == 360,
293
+ self.dec_min == -90,
294
+ self.dec_max == 90,
295
+ ]
296
+ )
297
+
298
+
299
+ class CreateMapMixin:
300
+ def create_map(self, height_degrees: float, width_degrees: float, *args, **kwargs):
301
+ """
302
+ Creates a map plot with this object at the center
303
+
304
+ Args:
305
+ height_degrees: Height of the map (degrees)
306
+ width_degrees: Width of the map (degrees)
307
+ *args: args passed through to [`MapPlot()`][starplot.MapPlot]
308
+ **kwargs: kwargs passed through to [`MapPlot()`][starplot.MapPlot]
309
+
310
+ Returns:
311
+ MapPlot: new instance of a [`MapPlot`][starplot.MapPlot]
312
+ """
313
+ from starplot import MapPlot, geometry
314
+
315
+ extent = geometry.rectangle(
316
+ center=(self.ra, self.dec),
317
+ height_degrees=height_degrees,
318
+ width_degrees=width_degrees,
319
+ )
320
+ minx, miny, maxx, maxy = extent.bounds
321
+ ra_min = minx
322
+ ra_max = maxx
323
+ dec_min = miny
324
+ dec_max = maxy
325
+
326
+ # handle wrapping
327
+ if ra_max < ra_min:
328
+ ra_max += 360
329
+
330
+ p = MapPlot(
331
+ ra_min=ra_min,
332
+ ra_max=ra_max,
333
+ dec_min=dec_min,
334
+ dec_max=dec_max,
335
+ *args,
336
+ **kwargs,
337
+ )
338
+ return p
339
+
340
+
341
+ class CreateOpticMixin:
342
+ def create_optic(self, *args, **kwargs):
343
+ """
344
+ Creates an optic plot with this object at the center
345
+
346
+ Args:
347
+ *args: args passed through to [`OpticPlot()`][starplot.OpticPlot]
348
+ **kwargs: kwargs passed through to [`OpticPlot()`][starplot.OpticPlot]
349
+
350
+ Returns:
351
+ OpticPlot: new instance of a [`OpticPlot`][starplot.OpticPlot]
352
+ """
353
+ from starplot import OpticPlot
354
+
355
+ return OpticPlot(
356
+ ra=self.ra,
357
+ dec=self.dec,
358
+ *args,
359
+ **kwargs,
360
+ )