starplot 0.15.8__py2.py3-none-any.whl → 0.16.1__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 +7 -2
- starplot/base.py +57 -60
- starplot/cli.py +3 -3
- starplot/config.py +56 -0
- starplot/data/__init__.py +5 -5
- starplot/data/bigsky.py +3 -3
- starplot/data/db.py +2 -2
- starplot/data/library/sky.db +0 -0
- starplot/geometry.py +48 -0
- starplot/horizon.py +194 -90
- starplot/map.py +71 -168
- starplot/mixins.py +0 -55
- starplot/models/dso.py +10 -2
- starplot/observer.py +71 -0
- starplot/optic.py +61 -26
- starplot/plotters/__init__.py +2 -0
- starplot/plotters/constellations.py +4 -6
- starplot/plotters/dsos.py +3 -2
- starplot/plotters/gradients.py +153 -0
- starplot/plotters/legend.py +247 -0
- starplot/plotters/milkyway.py +8 -5
- starplot/plotters/stars.py +5 -3
- starplot/projections.py +155 -55
- starplot/styles/base.py +98 -22
- starplot/styles/ext/antique.yml +0 -1
- starplot/styles/ext/blue_dark.yml +0 -1
- starplot/styles/ext/blue_gold.yml +60 -52
- starplot/styles/ext/blue_light.yml +0 -1
- starplot/styles/ext/blue_medium.yml +7 -7
- starplot/styles/ext/blue_night.yml +178 -0
- starplot/styles/ext/cb_wong.yml +0 -1
- starplot/styles/ext/gradient_presets.yml +158 -0
- starplot/styles/ext/grayscale.yml +0 -1
- starplot/styles/ext/grayscale_dark.yml +0 -1
- starplot/styles/ext/nord.yml +0 -1
- starplot/styles/extensions.py +90 -0
- starplot/zenith.py +174 -0
- {starplot-0.15.8.dist-info → starplot-0.16.1.dist-info}/METADATA +18 -11
- {starplot-0.15.8.dist-info → starplot-0.16.1.dist-info}/RECORD +42 -36
- starplot/settings.py +0 -26
- {starplot-0.15.8.dist-info → starplot-0.16.1.dist-info}/WHEEL +0 -0
- {starplot-0.15.8.dist-info → starplot-0.16.1.dist-info}/entry_points.txt +0 -0
- {starplot-0.15.8.dist-info → starplot-0.16.1.dist-info}/licenses/LICENSE +0 -0
starplot/map.py
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import datetime
|
|
2
1
|
import math
|
|
3
2
|
from typing import Callable
|
|
4
3
|
from functools import cache
|
|
5
4
|
|
|
6
5
|
from cartopy import crs as ccrs
|
|
7
6
|
from matplotlib import pyplot as plt
|
|
8
|
-
from matplotlib import
|
|
7
|
+
from matplotlib import patches, ticker
|
|
9
8
|
from matplotlib.ticker import FuncFormatter, FixedLocator
|
|
10
9
|
from shapely import Polygon
|
|
11
10
|
from skyfield.api import wgs84
|
|
@@ -15,18 +14,21 @@ from starplot.coordinates import CoordinateSystem
|
|
|
15
14
|
from starplot import geod
|
|
16
15
|
from starplot.base import BasePlot, DPI
|
|
17
16
|
from starplot.mixins import ExtentMaskMixin
|
|
17
|
+
from starplot.observer import Observer
|
|
18
18
|
from starplot.plotters import (
|
|
19
19
|
ConstellationPlotterMixin,
|
|
20
20
|
StarPlotterMixin,
|
|
21
21
|
DsoPlotterMixin,
|
|
22
22
|
MilkyWayPlotterMixin,
|
|
23
|
+
LegendPlotterMixin,
|
|
24
|
+
GradientBackgroundMixin,
|
|
23
25
|
)
|
|
24
|
-
from starplot.projections import
|
|
26
|
+
from starplot.projections import StereoNorth, StereoSouth, ProjectionBase
|
|
25
27
|
from starplot.styles import (
|
|
26
28
|
ObjectStyle,
|
|
27
|
-
LabelStyle,
|
|
28
29
|
PlotStyle,
|
|
29
30
|
PathStyle,
|
|
31
|
+
GradientDirection,
|
|
30
32
|
)
|
|
31
33
|
from starplot.styles.helpers import use_style
|
|
32
34
|
from starplot.utils import lon_to_ra, ra_to_lon
|
|
@@ -42,21 +44,18 @@ class MapPlot(
|
|
|
42
44
|
DsoPlotterMixin,
|
|
43
45
|
MilkyWayPlotterMixin,
|
|
44
46
|
ConstellationPlotterMixin,
|
|
47
|
+
LegendPlotterMixin,
|
|
48
|
+
GradientBackgroundMixin,
|
|
45
49
|
):
|
|
46
50
|
"""Creates a new map plot.
|
|
47
51
|
|
|
48
|
-
!!! star "Note"
|
|
49
|
-
**`lat`, `lon`, and `dt` are required for perspective projections (`Orthographic`, `Stereographic`, and `Zenith`)**
|
|
50
|
-
|
|
51
52
|
Args:
|
|
52
|
-
projection: Projection of the map
|
|
53
|
+
projection: [Projection](/reference-mapplot/#projections) of the map
|
|
53
54
|
ra_min: Minimum right ascension of the map's extent, in degrees (0...360)
|
|
54
55
|
ra_max: Maximum right ascension of the map's extent, in degrees (0...360)
|
|
55
56
|
dec_min: Minimum declination of the map's extent, in degrees (-90...90)
|
|
56
57
|
dec_max: Maximum declination of the map's extent, in degrees (-90...90)
|
|
57
|
-
|
|
58
|
-
lon: Longitude for perspective projections: Orthographic, Stereographic, and Zenith
|
|
59
|
-
dt: Date/time to use for star/planet positions, (*must be timezone-aware*). Default = current UTC time.
|
|
58
|
+
observer: Observer instance which specifies a time and place
|
|
60
59
|
ephemeris: Ephemeris to use for calculating planet positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
|
|
61
60
|
style: Styling for the plot (colors, sizes, fonts, etc)
|
|
62
61
|
resolution: Size (in pixels) of largest dimension of the map
|
|
@@ -72,17 +71,16 @@ class MapPlot(
|
|
|
72
71
|
"""
|
|
73
72
|
|
|
74
73
|
_coordinate_system = CoordinateSystem.RA_DEC
|
|
74
|
+
_gradient_direction = GradientDirection.LINEAR
|
|
75
75
|
|
|
76
76
|
def __init__(
|
|
77
77
|
self,
|
|
78
|
-
projection:
|
|
78
|
+
projection: ProjectionBase,
|
|
79
79
|
ra_min: float = 0,
|
|
80
80
|
ra_max: float = 360,
|
|
81
81
|
dec_min: float = -90,
|
|
82
82
|
dec_max: float = 90,
|
|
83
|
-
|
|
84
|
-
lon: float = None,
|
|
85
|
-
dt: datetime = None,
|
|
83
|
+
observer: Observer = Observer(),
|
|
86
84
|
ephemeris: str = "de421_2001.bsp",
|
|
87
85
|
style: PlotStyle = DEFAULT_MAP_STYLE,
|
|
88
86
|
resolution: int = 4096,
|
|
@@ -95,7 +93,7 @@ class MapPlot(
|
|
|
95
93
|
**kwargs,
|
|
96
94
|
) -> "MapPlot":
|
|
97
95
|
super().__init__(
|
|
98
|
-
|
|
96
|
+
observer,
|
|
99
97
|
ephemeris,
|
|
100
98
|
style,
|
|
101
99
|
resolution,
|
|
@@ -120,24 +118,8 @@ class MapPlot(
|
|
|
120
118
|
self.ra_max = ra_max
|
|
121
119
|
self.dec_min = dec_min
|
|
122
120
|
self.dec_max = dec_max
|
|
123
|
-
self.lat = lat
|
|
124
|
-
self.lon = lon
|
|
125
121
|
self.clip_path = clip_path
|
|
126
122
|
|
|
127
|
-
if self.projection in [
|
|
128
|
-
Projection.ORTHOGRAPHIC,
|
|
129
|
-
Projection.STEREOGRAPHIC,
|
|
130
|
-
Projection.ZENITH,
|
|
131
|
-
] and (lat is None or lon is None):
|
|
132
|
-
raise ValueError(
|
|
133
|
-
f"lat and lon are required for the {self.projection.value.upper()} projection"
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
if self.projection == Projection.ZENITH and not self._is_global_extent():
|
|
137
|
-
raise ValueError(
|
|
138
|
-
"Zenith projection requires a global extent: ra_min=0, ra_max=360, dec_min=-90, dec_max=90"
|
|
139
|
-
)
|
|
140
|
-
|
|
141
123
|
self._geodetic = ccrs.Geodetic()
|
|
142
124
|
self._plate_carree = ccrs.PlateCarree()
|
|
143
125
|
self._crs = ccrs.CRS(
|
|
@@ -192,9 +174,10 @@ class MapPlot(
|
|
|
192
174
|
self.dec_max = extent[3]
|
|
193
175
|
|
|
194
176
|
# adjust the RA min/max if the DEC bounds is near the poles
|
|
195
|
-
if
|
|
196
|
-
self.
|
|
197
|
-
|
|
177
|
+
if (
|
|
178
|
+
isinstance(self.projection, StereoNorth)
|
|
179
|
+
or isinstance(self.projection, StereoSouth)
|
|
180
|
+
) and (self.dec_max > 80 or self.dec_min < -80):
|
|
198
181
|
self.ra_min = 0
|
|
199
182
|
self.ra_max = 360
|
|
200
183
|
|
|
@@ -233,11 +216,13 @@ class MapPlot(
|
|
|
233
216
|
label: Label for the zenith
|
|
234
217
|
legend_label: Label in the legend
|
|
235
218
|
"""
|
|
236
|
-
if self.
|
|
237
|
-
raise ValueError("
|
|
219
|
+
if self.observer is None:
|
|
220
|
+
raise ValueError("observer is required for plotting the zenith")
|
|
238
221
|
|
|
239
|
-
geographic = wgs84.latlon(
|
|
240
|
-
|
|
222
|
+
geographic = wgs84.latlon(
|
|
223
|
+
latitude_degrees=self.observer.lat, longitude_degrees=self.observer.lon
|
|
224
|
+
)
|
|
225
|
+
observer = geographic.at(self.observer.timescale)
|
|
241
226
|
zenith = observer.from_altaz(alt_degrees=90, az_degrees=0)
|
|
242
227
|
ra, dec, _ = zenith.radec()
|
|
243
228
|
|
|
@@ -262,11 +247,13 @@ class MapPlot(
|
|
|
262
247
|
style: Style of the horizon path. If None, then the plot's style definition will be used.
|
|
263
248
|
labels: List of labels for cardinal directions. **NOTE: labels should be in the order: North, East, South, West.**
|
|
264
249
|
"""
|
|
265
|
-
if self.
|
|
266
|
-
raise ValueError("
|
|
250
|
+
if self.observer is None:
|
|
251
|
+
raise ValueError("observer is required for plotting the horizon")
|
|
267
252
|
|
|
268
|
-
geographic = wgs84.latlon(
|
|
269
|
-
|
|
253
|
+
geographic = wgs84.latlon(
|
|
254
|
+
latitude_degrees=self.observer.lat, longitude_degrees=self.observer.lon
|
|
255
|
+
)
|
|
256
|
+
observer = geographic.at(self.observer.timescale)
|
|
270
257
|
zenith = observer.from_altaz(alt_degrees=90, az_degrees=0)
|
|
271
258
|
ra, dec, _ = zenith.radec()
|
|
272
259
|
|
|
@@ -285,58 +272,16 @@ class MapPlot(
|
|
|
285
272
|
y.append(y0)
|
|
286
273
|
|
|
287
274
|
style_kwargs = {}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
facecolor=None,
|
|
299
|
-
fill=False,
|
|
300
|
-
transform=self.ax.transAxes,
|
|
301
|
-
**style_kwargs,
|
|
302
|
-
)
|
|
303
|
-
self.ax.add_patch(patch)
|
|
304
|
-
self._background_clip_path = patch
|
|
305
|
-
self._update_clip_path_polygon(
|
|
306
|
-
buffer=style.line.width / 2 + 2 * style.line.edge_width + 20
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
if not labels:
|
|
310
|
-
return
|
|
311
|
-
|
|
312
|
-
label_ax_coords = [
|
|
313
|
-
(0.5, 0.95), # north
|
|
314
|
-
(0.045, 0.5), # east
|
|
315
|
-
(0.5, 0.045), # south
|
|
316
|
-
(0.954, 0.5), # west
|
|
317
|
-
]
|
|
318
|
-
for label, coords in zip(labels, label_ax_coords):
|
|
319
|
-
self.ax.annotate(
|
|
320
|
-
label,
|
|
321
|
-
coords,
|
|
322
|
-
xycoords=self.ax.transAxes,
|
|
323
|
-
clip_on=False,
|
|
324
|
-
**style.label.matplot_kwargs(self.scale),
|
|
325
|
-
)
|
|
326
|
-
|
|
327
|
-
return
|
|
328
|
-
|
|
329
|
-
else:
|
|
330
|
-
style_kwargs["clip_on"] = True
|
|
331
|
-
style_kwargs["clip_path"] = self._background_clip_path
|
|
332
|
-
self.ax.plot(
|
|
333
|
-
x,
|
|
334
|
-
y,
|
|
335
|
-
dash_capstyle=style.line.dash_capstyle,
|
|
336
|
-
**style.line.matplot_kwargs(self.scale),
|
|
337
|
-
**style_kwargs,
|
|
338
|
-
**self._plot_kwargs(),
|
|
339
|
-
)
|
|
275
|
+
style_kwargs["clip_on"] = True
|
|
276
|
+
style_kwargs["clip_path"] = self._background_clip_path
|
|
277
|
+
self.ax.plot(
|
|
278
|
+
x,
|
|
279
|
+
y,
|
|
280
|
+
dash_capstyle=style.line.dash_capstyle,
|
|
281
|
+
**style.line.matplot_kwargs(self.scale),
|
|
282
|
+
**style_kwargs,
|
|
283
|
+
**self._plot_kwargs(),
|
|
284
|
+
)
|
|
340
285
|
|
|
341
286
|
if not labels:
|
|
342
287
|
return
|
|
@@ -420,6 +365,7 @@ class MapPlot(
|
|
|
420
365
|
gid="gridlines",
|
|
421
366
|
**line_style_kwargs,
|
|
422
367
|
)
|
|
368
|
+
gridlines.set_zorder(style.line.zorder)
|
|
423
369
|
|
|
424
370
|
if labels:
|
|
425
371
|
self._axis_labels = True
|
|
@@ -482,6 +428,20 @@ class MapPlot(
|
|
|
482
428
|
width, height = bbox.width, bbox.height
|
|
483
429
|
self.fig.set_size_inches(width, height)
|
|
484
430
|
|
|
431
|
+
def _set_extent(self):
|
|
432
|
+
bounds = self._latlon_bounds()
|
|
433
|
+
# (bounds[2] + bounds[3]) / 2
|
|
434
|
+
# center_lon = (bounds[0] + bounds[1]) / 2
|
|
435
|
+
|
|
436
|
+
# if hasattr(self.projection, "center_ra"):
|
|
437
|
+
# self.projection.center_ra = -1 * center_lon
|
|
438
|
+
|
|
439
|
+
if self._is_global_extent():
|
|
440
|
+
# this cartopy function works better for setting global extents
|
|
441
|
+
self.ax.set_global()
|
|
442
|
+
else:
|
|
443
|
+
self.ax.set_extent(bounds, crs=self._plate_carree)
|
|
444
|
+
|
|
485
445
|
def _init_plot(self):
|
|
486
446
|
self.fig = plt.figure(
|
|
487
447
|
figsize=(self.figure_size, self.figure_size),
|
|
@@ -489,74 +449,21 @@ class MapPlot(
|
|
|
489
449
|
layout="constrained",
|
|
490
450
|
dpi=DPI,
|
|
491
451
|
)
|
|
492
|
-
bounds = self._latlon_bounds()
|
|
493
|
-
center_lat = (bounds[2] + bounds[3]) / 2
|
|
494
|
-
center_lon = (bounds[0] + bounds[1]) / 2
|
|
495
|
-
self._center_lat = center_lat
|
|
496
|
-
self._center_lon = center_lon
|
|
497
|
-
|
|
498
|
-
if self.projection in [
|
|
499
|
-
Projection.ORTHOGRAPHIC,
|
|
500
|
-
Projection.STEREOGRAPHIC,
|
|
501
|
-
Projection.ZENITH,
|
|
502
|
-
]:
|
|
503
|
-
# Calculate local sidereal time (LST) to shift RA DEC to be in line with current date and time
|
|
504
|
-
lst = -(360.0 * self.timescale.gmst / 24.0 + self.lon) % 360.0
|
|
505
|
-
self._proj = Projection.crs(self.projection, lon=lst, lat=self.lat)
|
|
506
|
-
elif self.projection == Projection.LAMBERT_AZ_EQ_AREA:
|
|
507
|
-
self._proj = Projection.crs(
|
|
508
|
-
self.projection, center_lat=center_lat, center_lon=center_lon
|
|
509
|
-
)
|
|
510
|
-
else:
|
|
511
|
-
self._proj = Projection.crs(self.projection, center_lon)
|
|
512
|
-
self._proj.threshold = 1000
|
|
513
|
-
self.ax = plt.axes(projection=self._proj)
|
|
514
452
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
theta = np.linspace(0, 2 * np.pi, 100)
|
|
518
|
-
center, radius = [0.5, 0.5], 0.45
|
|
519
|
-
verts = np.vstack([np.sin(theta), np.cos(theta)]).T
|
|
520
|
-
circle = path.Path(verts * radius + center)
|
|
521
|
-
extent = self.ax.get_extent(crs=self._proj)
|
|
522
|
-
self.ax.set_extent((p / 3.548 for p in extent), crs=self._proj)
|
|
523
|
-
self.ax.set_boundary(circle, transform=self.ax.transAxes)
|
|
524
|
-
else:
|
|
525
|
-
# this cartopy function works better for setting global extents
|
|
526
|
-
self.ax.set_global()
|
|
527
|
-
else:
|
|
528
|
-
self.ax.set_extent(bounds, crs=self._plate_carree)
|
|
453
|
+
self._proj = self.projection.crs
|
|
454
|
+
self.ax = self.fig.add_subplot(1, 1, 1, projection=self._proj)
|
|
529
455
|
|
|
530
|
-
self.
|
|
456
|
+
self._set_extent()
|
|
531
457
|
self._adjust_radec_minmax()
|
|
532
458
|
|
|
533
|
-
self.logger.debug(f"Projection = {self.projection.
|
|
459
|
+
self.logger.debug(f"Projection = {self.projection.__class__.__name__.upper()}")
|
|
534
460
|
|
|
535
461
|
self._fit_to_ax()
|
|
536
|
-
self._plot_background_clip_path()
|
|
537
|
-
|
|
538
|
-
@use_style(LabelStyle, "info_text")
|
|
539
|
-
def info(self, style: LabelStyle = None):
|
|
540
|
-
"""
|
|
541
|
-
Plots info text in the lower left corner, including date/time and lat/lon.
|
|
542
462
|
|
|
543
|
-
|
|
463
|
+
# if self.gradient_preset:
|
|
464
|
+
# self.apply_gradient_background(self.gradient_preset)
|
|
544
465
|
|
|
545
|
-
|
|
546
|
-
style: Styling of the info text. If None, then the plot's style definition will be used.
|
|
547
|
-
"""
|
|
548
|
-
if not self.projection == Projection.ZENITH:
|
|
549
|
-
raise NotImplementedError("info text only available for zenith projections")
|
|
550
|
-
|
|
551
|
-
dt_str = self.dt.strftime("%m/%d/%Y @ %H:%M:%S") + " " + self.dt.tzname()
|
|
552
|
-
info = f"{str(self.lat)}, {str(self.lon)}\n{dt_str}"
|
|
553
|
-
self.ax.text(
|
|
554
|
-
0.05,
|
|
555
|
-
0.05,
|
|
556
|
-
info,
|
|
557
|
-
transform=self.ax.transAxes,
|
|
558
|
-
**style.matplot_kwargs(self.scale),
|
|
559
|
-
)
|
|
466
|
+
self._plot_background_clip_path()
|
|
560
467
|
|
|
561
468
|
def _ax_to_radec(self, x, y):
|
|
562
469
|
trans = self.ax.transAxes + self.ax.transData.inverted()
|
|
@@ -565,6 +472,12 @@ class MapPlot(
|
|
|
565
472
|
return (x_ra + 360), y_ra
|
|
566
473
|
|
|
567
474
|
def _plot_background_clip_path(self):
|
|
475
|
+
if self.style.has_gradient_background():
|
|
476
|
+
background_color = "#ffffff00"
|
|
477
|
+
self._plot_gradient_background(self.style.background_color)
|
|
478
|
+
else:
|
|
479
|
+
background_color = self.style.background_color.as_hex()
|
|
480
|
+
|
|
568
481
|
def to_axes(points):
|
|
569
482
|
ax_points = []
|
|
570
483
|
|
|
@@ -579,22 +492,11 @@ class MapPlot(
|
|
|
579
492
|
points = list(zip(*self.clip_path.exterior.coords.xy))
|
|
580
493
|
self._background_clip_path = patches.Polygon(
|
|
581
494
|
to_axes(points),
|
|
582
|
-
facecolor=
|
|
495
|
+
facecolor=background_color,
|
|
583
496
|
fill=True,
|
|
584
497
|
zorder=-2_000,
|
|
585
498
|
transform=self.ax.transAxes,
|
|
586
499
|
)
|
|
587
|
-
elif self.projection == Projection.ZENITH:
|
|
588
|
-
self._background_clip_path = patches.Circle(
|
|
589
|
-
(0.50, 0.50),
|
|
590
|
-
radius=0.45,
|
|
591
|
-
fill=True,
|
|
592
|
-
facecolor=self.style.background_color.as_hex(),
|
|
593
|
-
# edgecolor=self.style.border_line_color.as_hex(),
|
|
594
|
-
linewidth=0,
|
|
595
|
-
zorder=-2_000,
|
|
596
|
-
transform=self.ax.transAxes,
|
|
597
|
-
)
|
|
598
500
|
else:
|
|
599
501
|
# draw patch in axes coords, which are easier to work with
|
|
600
502
|
# in cases like this cause they go from 0...1 in all plots
|
|
@@ -602,12 +504,13 @@ class MapPlot(
|
|
|
602
504
|
(0, 0),
|
|
603
505
|
width=1,
|
|
604
506
|
height=1,
|
|
605
|
-
facecolor=
|
|
507
|
+
facecolor=background_color,
|
|
606
508
|
linewidth=0,
|
|
607
509
|
fill=True,
|
|
608
510
|
zorder=-2_000,
|
|
609
511
|
transform=self.ax.transAxes,
|
|
610
512
|
)
|
|
611
513
|
|
|
514
|
+
self.ax.set_facecolor(background_color)
|
|
612
515
|
self.ax.add_patch(self._background_clip_path)
|
|
613
516
|
self._update_clip_path_polygon()
|
starplot/mixins.py
CHANGED
|
@@ -44,61 +44,6 @@ class ExtentMaskMixin:
|
|
|
44
44
|
]
|
|
45
45
|
)
|
|
46
46
|
|
|
47
|
-
@cache
|
|
48
|
-
def _extent_mask_altaz(self):
|
|
49
|
-
"""
|
|
50
|
-
Returns shapely geometry objects of the alt/az extent
|
|
51
|
-
|
|
52
|
-
If the extent crosses North cardinal direction, then a MultiPolygon will be returned
|
|
53
|
-
"""
|
|
54
|
-
extent = list(self.ax.get_extent(crs=self._plate_carree))
|
|
55
|
-
alt_min, alt_max = extent[2], extent[3]
|
|
56
|
-
az_min, az_max = extent[0], extent[1]
|
|
57
|
-
|
|
58
|
-
if az_min < 0:
|
|
59
|
-
az_min += 360
|
|
60
|
-
if az_max < 0:
|
|
61
|
-
az_max += 360
|
|
62
|
-
|
|
63
|
-
if az_min >= az_max:
|
|
64
|
-
az_max += 360
|
|
65
|
-
|
|
66
|
-
self.az = (az_min, az_max)
|
|
67
|
-
self.alt = (alt_min, alt_max)
|
|
68
|
-
|
|
69
|
-
if az_max <= 360:
|
|
70
|
-
coords = [
|
|
71
|
-
[az_min, alt_min],
|
|
72
|
-
[az_max, alt_min],
|
|
73
|
-
[az_max, alt_max],
|
|
74
|
-
[az_min, alt_max],
|
|
75
|
-
[az_min, alt_min],
|
|
76
|
-
]
|
|
77
|
-
return Polygon(coords)
|
|
78
|
-
|
|
79
|
-
else:
|
|
80
|
-
coords_1 = [
|
|
81
|
-
[az_min, alt_min],
|
|
82
|
-
[360, alt_min],
|
|
83
|
-
[360, alt_max],
|
|
84
|
-
[az_min, alt_max],
|
|
85
|
-
[az_min, alt_min],
|
|
86
|
-
]
|
|
87
|
-
coords_2 = [
|
|
88
|
-
[0, alt_min],
|
|
89
|
-
[az_max - 360, alt_min],
|
|
90
|
-
[az_max - 360, alt_max],
|
|
91
|
-
[0, alt_max],
|
|
92
|
-
[0, alt_min],
|
|
93
|
-
]
|
|
94
|
-
|
|
95
|
-
return MultiPolygon(
|
|
96
|
-
[
|
|
97
|
-
Polygon(coords_1),
|
|
98
|
-
Polygon(coords_2),
|
|
99
|
-
]
|
|
100
|
-
)
|
|
101
|
-
|
|
102
47
|
def _is_global_extent(self):
|
|
103
48
|
"""Returns True if the plot's RA/DEC range is the entire celestial sphere"""
|
|
104
49
|
return all(
|
starplot/models/dso.py
CHANGED
|
@@ -89,6 +89,13 @@ class DSO(SkyObject, CreateMapMixin, CreateOpticMixin):
|
|
|
89
89
|
type: DsoType
|
|
90
90
|
"""Type of DSO"""
|
|
91
91
|
|
|
92
|
+
common_names: list[str] = None
|
|
93
|
+
"""
|
|
94
|
+
List of common names for the DSO (e.g. 'Andromeda Galaxy' for M31)
|
|
95
|
+
|
|
96
|
+
Note: this field is parsed into a list of strings _after_ querying DSOs, so if you want to query on this field, you should treat it as a comma-separated list.
|
|
97
|
+
"""
|
|
98
|
+
|
|
92
99
|
magnitude: Optional[float] = None
|
|
93
100
|
"""Magnitude (if available)"""
|
|
94
101
|
|
|
@@ -186,6 +193,7 @@ class DSO(SkyObject, CreateMapMixin, CreateOpticMixin):
|
|
|
186
193
|
def from_tuple(d: tuple) -> DSO:
|
|
187
194
|
dso = DSO(
|
|
188
195
|
name=d.name,
|
|
196
|
+
common_names=d.common_names.split(",") if d.common_names else [],
|
|
189
197
|
ra=d.ra,
|
|
190
198
|
dec=d.dec,
|
|
191
199
|
type=d.type,
|
|
@@ -243,10 +251,11 @@ DSO_LEGEND_LABELS = {
|
|
|
243
251
|
DsoType.GROUP_OF_GALAXIES: "Galaxy",
|
|
244
252
|
# Nebulas ----------
|
|
245
253
|
DsoType.NEBULA: "Nebula",
|
|
246
|
-
DsoType.PLANETARY_NEBULA: "Nebula",
|
|
254
|
+
DsoType.PLANETARY_NEBULA: "Planetary Nebula",
|
|
247
255
|
DsoType.EMISSION_NEBULA: "Nebula",
|
|
248
256
|
DsoType.STAR_CLUSTER_NEBULA: "Nebula",
|
|
249
257
|
DsoType.REFLECTION_NEBULA: "Nebula",
|
|
258
|
+
DsoType.HII_IONIZED_REGION: "Nebula",
|
|
250
259
|
# Star Clusters ----------
|
|
251
260
|
DsoType.OPEN_CLUSTER: "Open Cluster",
|
|
252
261
|
DsoType.GLOBULAR_CLUSTER: "Globular Cluster",
|
|
@@ -255,7 +264,6 @@ DSO_LEGEND_LABELS = {
|
|
|
255
264
|
DsoType.ASSOCIATION_OF_STARS: "Association of stars",
|
|
256
265
|
DsoType.NOVA_STAR: "Nova Star",
|
|
257
266
|
# Others
|
|
258
|
-
DsoType.HII_IONIZED_REGION: "HII Ionized Region",
|
|
259
267
|
DsoType.DARK_NEBULA: "Dark Nebula",
|
|
260
268
|
DsoType.SUPERNOVA_REMNANT: "Supernova Remnant",
|
|
261
269
|
}
|
starplot/observer.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from functools import cached_property
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, AwareDatetime, Field, computed_field
|
|
5
|
+
from skyfield.timelib import Timescale
|
|
6
|
+
|
|
7
|
+
from starplot.data import load
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Observer(BaseModel):
|
|
11
|
+
"""
|
|
12
|
+
Represents an observer at a specific time and place.
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
obs = Observer(
|
|
18
|
+
dt=datetime(2025, 10, 13, 21, 0, 0, tzinfo=ZoneInfo('US/Pacific')),
|
|
19
|
+
lat=33.363484,
|
|
20
|
+
lon=-116.836394,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
dt: AwareDatetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
27
|
+
"""
|
|
28
|
+
Date and time of observation (**must be timezone-aware**).
|
|
29
|
+
|
|
30
|
+
Defaults to current time in UTC.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
lat: float = Field(default=0, ge=-90, le=90)
|
|
34
|
+
"""Latitude of observer location"""
|
|
35
|
+
|
|
36
|
+
lon: float = Field(default=0, ge=-180, le=180)
|
|
37
|
+
"""Longitude of observer location"""
|
|
38
|
+
|
|
39
|
+
class Config:
|
|
40
|
+
arbitrary_types_allowed = True
|
|
41
|
+
|
|
42
|
+
@computed_field
|
|
43
|
+
@cached_property
|
|
44
|
+
def timescale(self) -> Timescale:
|
|
45
|
+
"""
|
|
46
|
+
**Read-only Property**
|
|
47
|
+
|
|
48
|
+
Timescale instance of the specified datetime (used by Skyfield)
|
|
49
|
+
"""
|
|
50
|
+
return load.timescale().from_datetime(self.dt)
|
|
51
|
+
|
|
52
|
+
@computed_field
|
|
53
|
+
@cached_property
|
|
54
|
+
def lst(self) -> float:
|
|
55
|
+
"""
|
|
56
|
+
**Read-only Property**
|
|
57
|
+
|
|
58
|
+
Local sidereal time (in degrees)
|
|
59
|
+
"""
|
|
60
|
+
return float(360.0 * self.timescale.gmst / 24.0 + self.lon) % 360.0
|
|
61
|
+
|
|
62
|
+
# @computed_field
|
|
63
|
+
# @cached_property
|
|
64
|
+
# def location(self):
|
|
65
|
+
# earth = self.ephemeris["earth"]
|
|
66
|
+
# return earth + wgs84.latlon(self.lat, self.lon)
|
|
67
|
+
|
|
68
|
+
# def observe(self):
|
|
69
|
+
# earth = self.ephemeris["earth"]
|
|
70
|
+
# self.location = earth + wgs84.latlon(self.lat, self.lon)
|
|
71
|
+
# return self.location.at(self.timescale).observe
|