starplot 0.20.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- starplot/__init__.py +59 -0
- starplot/callables.py +176 -0
- starplot/cli.py +29 -0
- starplot/config.py +80 -0
- starplot/coordinates.py +7 -0
- starplot/data/__init__.py +22 -0
- starplot/data/catalogs.py +356 -0
- starplot/data/constellations.py +71 -0
- starplot/data/db.py +33 -0
- starplot/data/dsos.py +85 -0
- starplot/data/ecliptic.py +201 -0
- starplot/data/library/constellation_names.parquet +0 -0
- starplot/data/library/dso_names.parquet +0 -0
- starplot/data/library/readme.md +1 -0
- starplot/data/library/star_designations.parquet +0 -0
- starplot/data/stars.py +79 -0
- starplot/data/translations.py +408 -0
- starplot/data/utils.py +35 -0
- starplot/geometry.py +319 -0
- starplot/mixins.py +360 -0
- starplot/models/__init__.py +21 -0
- starplot/models/base.py +77 -0
- starplot/models/comet.py +302 -0
- starplot/models/constellation.py +151 -0
- starplot/models/dso.py +317 -0
- starplot/models/milky_way.py +30 -0
- starplot/models/moon.py +130 -0
- starplot/models/objects.py +29 -0
- starplot/models/observer.py +125 -0
- starplot/models/optics.py +342 -0
- starplot/models/planet.py +137 -0
- starplot/models/satellite.py +138 -0
- starplot/models/star.py +253 -0
- starplot/models/sun.py +62 -0
- starplot/plots/__init__.py +7 -0
- starplot/plots/base.py +1023 -0
- starplot/plots/galaxy.py +372 -0
- starplot/plots/horizon.py +543 -0
- starplot/plots/map.py +515 -0
- starplot/plots/optic.py +469 -0
- starplot/plots/zenith.py +217 -0
- starplot/plotters/__init__.py +9 -0
- starplot/plotters/arrow.py +174 -0
- starplot/plotters/constellations.py +298 -0
- starplot/plotters/debug.py +21 -0
- starplot/plotters/dsos.py +294 -0
- starplot/plotters/experimental.py +722 -0
- starplot/plotters/gradients.py +153 -0
- starplot/plotters/legend.py +253 -0
- starplot/plotters/milkyway.py +51 -0
- starplot/plotters/stars.py +319 -0
- starplot/plotters/text.py +802 -0
- starplot/profile.py +16 -0
- starplot/projections.py +184 -0
- starplot/styles/__init__.py +6 -0
- starplot/styles/base.py +1344 -0
- starplot/styles/ext/antique.yml +175 -0
- starplot/styles/ext/blue_dark.yml +163 -0
- starplot/styles/ext/blue_gold.yml +147 -0
- starplot/styles/ext/blue_light.yml +123 -0
- starplot/styles/ext/blue_medium.yml +142 -0
- starplot/styles/ext/blue_night.yml +185 -0
- starplot/styles/ext/cb_wong.yml +124 -0
- starplot/styles/ext/color_print.yml +111 -0
- starplot/styles/ext/gradient_presets.yml +158 -0
- starplot/styles/ext/grayscale.yml +94 -0
- starplot/styles/ext/grayscale_dark.yml +136 -0
- starplot/styles/ext/map.yml +12 -0
- starplot/styles/ext/nord.yml +158 -0
- starplot/styles/ext/optic.yml +20 -0
- starplot/styles/ext/publication.yml +8 -0
- starplot/styles/extensions.py +129 -0
- starplot/styles/fonts-library/gfs-didot/DESCRIPTION.en_us.html +9 -0
- starplot/styles/fonts-library/gfs-didot/GFSDidot-Regular.ttf +0 -0
- starplot/styles/fonts-library/gfs-didot/METADATA.pb +16 -0
- starplot/styles/fonts-library/gfs-didot/OFL.txt +94 -0
- starplot/styles/fonts-library/inter/Inter-Bold.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-BoldItalic.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-ExtraBold.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-ExtraLight.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-ExtraLightItalic.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-Italic.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-Light.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-LightItalic.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-Regular.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-SemiBold.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-SemiBoldItalic.ttf +0 -0
- starplot/styles/fonts-library/inter/LICENSE.txt +92 -0
- starplot/styles/fonts.py +15 -0
- starplot/styles/helpers.py +93 -0
- starplot/styles/markers.py +308 -0
- starplot/utils.py +169 -0
- starplot/warnings.py +21 -0
- starplot-0.20.5.dist-info/METADATA +146 -0
- starplot-0.20.5.dist-info/RECORD +98 -0
- starplot-0.20.5.dist-info/WHEEL +4 -0
- starplot-0.20.5.dist-info/entry_points.txt +3 -0
- starplot-0.20.5.dist-info/licenses/LICENSE +21 -0
starplot/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
|
+
)
|