starplot 0.10.2__py2.py3-none-any.whl → 0.11.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 +10 -2
- starplot/base.py +26 -5
- starplot/data/__init__.py +12 -1
- starplot/data/bigsky.py +97 -0
- starplot/data/library/{stars.tycho-1.gz.parquet → stars.bigsky.mag11.parquet} +0 -0
- starplot/data/stars.py +30 -16
- starplot/data/utils.py +27 -0
- starplot/map.py +210 -77
- starplot/models/__init__.py +1 -0
- starplot/models/base.py +17 -0
- starplot/models/constellation.py +72 -0
- starplot/models/star.py +8 -0
- starplot/optic.py +2 -2
- starplot/plotters/stars.py +25 -5
- starplot/projections.py +11 -0
- starplot/styles/base.py +71 -4
- starplot/styles/ext/antique.yml +16 -0
- starplot/styles/ext/blue_dark.yml +16 -1
- starplot/styles/ext/blue_light.yml +18 -0
- starplot/styles/ext/blue_medium.yml +18 -0
- starplot/styles/ext/cb_wong.yml +20 -1
- starplot/styles/ext/color_print.yml +108 -0
- starplot/styles/ext/grayscale.yml +18 -0
- starplot/styles/ext/grayscale_dark.yml +20 -4
- starplot/styles/ext/nord.yml +21 -4
- starplot/styles/ext/optic.yml +1 -1
- starplot/styles/extensions.py +1 -0
- {starplot-0.10.2.dist-info → starplot-0.11.0.dist-info}/METADATA +7 -3
- {starplot-0.10.2.dist-info → starplot-0.11.0.dist-info}/RECORD +31 -28
- starplot/data/library/readme.md +0 -9
- {starplot-0.10.2.dist-info → starplot-0.11.0.dist-info}/LICENSE +0 -0
- {starplot-0.10.2.dist-info → starplot-0.11.0.dist-info}/WHEEL +0 -0
starplot/map.py
CHANGED
|
@@ -13,6 +13,7 @@ from skyfield.api import Star as SkyfieldStar, wgs84
|
|
|
13
13
|
import geopandas as gpd
|
|
14
14
|
import numpy as np
|
|
15
15
|
|
|
16
|
+
from starplot import geod
|
|
16
17
|
from starplot.base import BasePlot
|
|
17
18
|
from starplot.data import DataFiles, constellations as condata, stars
|
|
18
19
|
from starplot.data.constellations import CONSTELLATIONS_FULL_NAMES
|
|
@@ -20,6 +21,7 @@ from starplot.mixins import ExtentMaskMixin
|
|
|
20
21
|
from starplot.plotters import StarPlotterMixin, DsoPlotterMixin
|
|
21
22
|
from starplot.projections import Projection
|
|
22
23
|
from starplot.styles import (
|
|
24
|
+
ObjectStyle,
|
|
23
25
|
LabelStyle,
|
|
24
26
|
LineStyle,
|
|
25
27
|
PlotStyle,
|
|
@@ -119,8 +121,6 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
119
121
|
"Zenith projection requires a global extent: ra_min=0, ra_max=24, dec_min=-90, dec_max=90"
|
|
120
122
|
)
|
|
121
123
|
|
|
122
|
-
self.stars_df = stars.load("hipparcos")
|
|
123
|
-
|
|
124
124
|
self._geodetic = ccrs.Geodetic()
|
|
125
125
|
self._plate_carree = ccrs.PlateCarree()
|
|
126
126
|
self._crs = ccrs.CRS(
|
|
@@ -356,6 +356,7 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
356
356
|
use_arrow=True,
|
|
357
357
|
bbox=self._extent_mask(),
|
|
358
358
|
)
|
|
359
|
+
stars_df = stars.load("hipparcos")
|
|
359
360
|
|
|
360
361
|
if constellations_gdf.empty:
|
|
361
362
|
return
|
|
@@ -372,8 +373,8 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
372
373
|
hiplines = conline_hips[c.id]
|
|
373
374
|
|
|
374
375
|
for s1_hip, s2_hip in hiplines:
|
|
375
|
-
s1 =
|
|
376
|
-
s2 =
|
|
376
|
+
s1 = stars_df.loc[s1_hip]
|
|
377
|
+
s2 = stars_df.loc[s2_hip]
|
|
377
378
|
|
|
378
379
|
s1_ra = s1.ra_hours * 15
|
|
379
380
|
s2_ra = s2.ra_hours * 15
|
|
@@ -438,32 +439,142 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
438
439
|
**style_kwargs,
|
|
439
440
|
)
|
|
440
441
|
|
|
441
|
-
@use_style(
|
|
442
|
+
@use_style(ObjectStyle, "zenith")
|
|
443
|
+
def zenith(
|
|
444
|
+
self,
|
|
445
|
+
style: ObjectStyle = None,
|
|
446
|
+
label: str = None,
|
|
447
|
+
legend_label: str = "Zenith",
|
|
448
|
+
):
|
|
449
|
+
"""
|
|
450
|
+
Plots a marker for the zenith (requires `lat`, `lon`, and `dt` to be defined when creating the plot)
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
style: Style of the zenith marker. If None, then the plot's style definition will be used.
|
|
454
|
+
label: Label for the zenith
|
|
455
|
+
legend_label: Label in the legend
|
|
456
|
+
"""
|
|
457
|
+
if self.lat is None or self.lon is None or self.dt is None:
|
|
458
|
+
raise ValueError("lat, lon, and dt are required for plotting the zenith")
|
|
459
|
+
|
|
460
|
+
geographic = wgs84.latlon(latitude_degrees=self.lat, longitude_degrees=self.lon)
|
|
461
|
+
observer = geographic.at(self.timescale)
|
|
462
|
+
zenith = observer.from_altaz(alt_degrees=90, az_degrees=0)
|
|
463
|
+
ra, dec, _ = zenith.radec()
|
|
464
|
+
|
|
465
|
+
self.marker(
|
|
466
|
+
ra.hours,
|
|
467
|
+
dec.degrees,
|
|
468
|
+
label,
|
|
469
|
+
style,
|
|
470
|
+
legend_label,
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
@use_style(PathStyle, "horizon")
|
|
442
474
|
def horizon(
|
|
443
475
|
self,
|
|
444
|
-
style:
|
|
445
|
-
|
|
446
|
-
edge_color="red",
|
|
447
|
-
line_style="dashed",
|
|
448
|
-
edge_width=4,
|
|
449
|
-
zorder=1000,
|
|
450
|
-
),
|
|
476
|
+
style: PathStyle = None,
|
|
477
|
+
labels: list = ["N", "E", "S", "W"],
|
|
451
478
|
):
|
|
452
479
|
"""
|
|
453
480
|
Draws a [great circle](https://en.wikipedia.org/wiki/Great_circle) representing the horizon for the given `lat`, `lon` at time `dt` (so you must define these when creating the plot to use this function)
|
|
454
481
|
|
|
455
482
|
Args:
|
|
456
|
-
style: Style of the
|
|
483
|
+
style: Style of the horizon path. If None, then the plot's style definition will be used.
|
|
484
|
+
labels: List of labels for cardinal directions. **NOTE: labels should be in the order: North, East, South, West.**
|
|
457
485
|
"""
|
|
458
486
|
if self.lat is None or self.lon is None or self.dt is None:
|
|
459
487
|
raise ValueError("lat, lon, and dt are required for plotting the horizon")
|
|
460
488
|
|
|
461
|
-
self.
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
489
|
+
geographic = wgs84.latlon(latitude_degrees=self.lat, longitude_degrees=self.lon)
|
|
490
|
+
observer = geographic.at(self.timescale)
|
|
491
|
+
zenith = observer.from_altaz(alt_degrees=90, az_degrees=0)
|
|
492
|
+
ra, dec, _ = zenith.radec()
|
|
493
|
+
|
|
494
|
+
points = geod.ellipse(
|
|
495
|
+
center=(ra.hours, dec.degrees),
|
|
496
|
+
height_degrees=180,
|
|
497
|
+
width_degrees=180,
|
|
498
|
+
num_pts=100,
|
|
499
|
+
)
|
|
500
|
+
x = []
|
|
501
|
+
y = []
|
|
502
|
+
verts = []
|
|
503
|
+
|
|
504
|
+
# TODO : handle map edges better
|
|
505
|
+
|
|
506
|
+
for ra, dec in points:
|
|
507
|
+
ra = ra / 15
|
|
508
|
+
x0, y0 = self._prepare_coords(ra, dec)
|
|
509
|
+
x.append(x0)
|
|
510
|
+
y.append(y0)
|
|
511
|
+
verts.append((x0, y0))
|
|
512
|
+
|
|
513
|
+
style_kwargs = {}
|
|
514
|
+
if self.projection == Projection.ZENITH:
|
|
515
|
+
"""
|
|
516
|
+
For zenith projections, we plot the horizon as a patch because
|
|
517
|
+
plottting as a line results in extra pixels on bottom.
|
|
518
|
+
|
|
519
|
+
TODO : investigate why line is extra thick on bottom when plotting line
|
|
520
|
+
"""
|
|
521
|
+
style_kwargs = style.line.matplot_kwargs(self._size_multiplier)
|
|
522
|
+
style_kwargs["clip_on"] = False
|
|
523
|
+
style_kwargs["edgecolor"] = style_kwargs.pop("color")
|
|
524
|
+
|
|
525
|
+
patch = patches.Polygon(
|
|
526
|
+
verts,
|
|
527
|
+
facecolor=None,
|
|
528
|
+
fill=False,
|
|
529
|
+
transform=self._crs,
|
|
530
|
+
**style_kwargs,
|
|
531
|
+
)
|
|
532
|
+
self.ax.add_patch(patch)
|
|
533
|
+
|
|
534
|
+
else:
|
|
535
|
+
style_kwargs["clip_on"] = True
|
|
536
|
+
style_kwargs["clip_path"] = self._background_clip_path
|
|
537
|
+
self.ax.plot(
|
|
538
|
+
x,
|
|
539
|
+
y,
|
|
540
|
+
dash_capstyle=style.line.dash_capstyle,
|
|
541
|
+
**style.line.matplot_kwargs(self._size_multiplier),
|
|
542
|
+
**style_kwargs,
|
|
543
|
+
**self._plot_kwargs(),
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
# self.circle(
|
|
547
|
+
# (ra.hours, dec.degrees),
|
|
548
|
+
# 90,
|
|
549
|
+
# style,
|
|
550
|
+
# num_pts=200,
|
|
551
|
+
# )
|
|
552
|
+
|
|
553
|
+
if not labels:
|
|
554
|
+
return
|
|
555
|
+
|
|
556
|
+
north = observer.from_altaz(alt_degrees=0, az_degrees=0)
|
|
557
|
+
east = observer.from_altaz(alt_degrees=0, az_degrees=90)
|
|
558
|
+
south = observer.from_altaz(alt_degrees=0, az_degrees=180)
|
|
559
|
+
west = observer.from_altaz(alt_degrees=0, az_degrees=270)
|
|
560
|
+
|
|
561
|
+
cardinal_directions = [north, east, south, west]
|
|
562
|
+
|
|
563
|
+
text_kwargs = dict(
|
|
564
|
+
**style.label.matplot_kwargs(self._size_multiplier),
|
|
565
|
+
hide_on_collision=False,
|
|
566
|
+
xytext=(style.label.offset_x, style.label.offset_y),
|
|
567
|
+
textcoords="offset pixels",
|
|
568
|
+
path_effects=[],
|
|
465
569
|
)
|
|
466
570
|
|
|
571
|
+
if self.projection == Projection.ZENITH:
|
|
572
|
+
text_kwargs["clip_on"] = False
|
|
573
|
+
|
|
574
|
+
for i, position in enumerate(cardinal_directions):
|
|
575
|
+
ra, dec, _ = position.radec()
|
|
576
|
+
self._text(ra.hours, dec.degrees, labels[i], **text_kwargs)
|
|
577
|
+
|
|
467
578
|
@use_style(PathStyle, "gridlines")
|
|
468
579
|
def gridlines(
|
|
469
580
|
self,
|
|
@@ -492,7 +603,7 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
492
603
|
"""
|
|
493
604
|
|
|
494
605
|
ra_formatter_fn_default = lambda r: f"{math.floor(r)}h" # noqa: E731
|
|
495
|
-
dec_formatter_fn_default = lambda d: f"{round(d)}\u00b0" # noqa: E731
|
|
606
|
+
dec_formatter_fn_default = lambda d: f"{round(d)}\u00b0 " # noqa: E731
|
|
496
607
|
|
|
497
608
|
ra_formatter_fn = ra_formatter_fn or ra_formatter_fn_default
|
|
498
609
|
dec_formatter_fn = dec_formatter_fn or dec_formatter_fn_default
|
|
@@ -573,7 +684,9 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
573
684
|
layout="constrained",
|
|
574
685
|
)
|
|
575
686
|
bounds = self._latlon_bounds()
|
|
687
|
+
center_lat = (bounds[2] + bounds[3]) / 2
|
|
576
688
|
center_lon = (bounds[0] + bounds[1]) / 2
|
|
689
|
+
self._center_lat = center_lat
|
|
577
690
|
self._center_lon = center_lon
|
|
578
691
|
|
|
579
692
|
if self.projection in [
|
|
@@ -584,6 +697,10 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
584
697
|
# Calculate LST to shift RA DEC to be in line with current date and time
|
|
585
698
|
lst = -(360.0 * self.timescale.gmst / 24.0 + self.lon) % 360.0
|
|
586
699
|
self._proj = Projection.crs(self.projection, lon=lst, lat=self.lat)
|
|
700
|
+
elif self.projection == Projection.LAMBERT_AZ_EQ_AREA:
|
|
701
|
+
self._proj = Projection.crs(
|
|
702
|
+
self.projection, center_lat=center_lat, center_lon=center_lon
|
|
703
|
+
)
|
|
587
704
|
else:
|
|
588
705
|
self._proj = Projection.crs(self.projection, center_lon)
|
|
589
706
|
self._proj.threshold = 1000
|
|
@@ -592,11 +709,11 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
592
709
|
if self._is_global_extent():
|
|
593
710
|
if self.projection == Projection.ZENITH:
|
|
594
711
|
theta = np.linspace(0, 2 * np.pi, 100)
|
|
595
|
-
center, radius = [0.5, 0.5], 0.
|
|
712
|
+
center, radius = [0.5, 0.5], 0.45
|
|
596
713
|
verts = np.vstack([np.sin(theta), np.cos(theta)]).T
|
|
597
714
|
circle = path.Path(verts * radius + center)
|
|
598
715
|
extent = self.ax.get_extent(crs=self._proj)
|
|
599
|
-
self.ax.set_extent((p / 3.
|
|
716
|
+
self.ax.set_extent((p / 3.548 for p in extent), crs=self._proj)
|
|
600
717
|
self.ax.set_boundary(circle, transform=self.ax.transAxes)
|
|
601
718
|
else:
|
|
602
719
|
# this cartopy function works better for setting global extents
|
|
@@ -609,28 +726,14 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
609
726
|
|
|
610
727
|
self.logger.debug(f"Projection = {self.projection.value.upper()}")
|
|
611
728
|
|
|
612
|
-
|
|
613
|
-
self._plot_border()
|
|
614
|
-
else:
|
|
615
|
-
# draw patch in axes coords, which are easier to work with
|
|
616
|
-
# in cases like this cause they go from 0...1 in all plots
|
|
617
|
-
self._background_clip_path = patches.Rectangle(
|
|
618
|
-
(0, 0),
|
|
619
|
-
width=1,
|
|
620
|
-
height=1,
|
|
621
|
-
facecolor=self.style.background_color.as_hex(),
|
|
622
|
-
linewidth=0,
|
|
623
|
-
fill=True,
|
|
624
|
-
zorder=-2_000,
|
|
625
|
-
transform=self.ax.transAxes,
|
|
626
|
-
)
|
|
627
|
-
self.ax.add_patch(self._background_clip_path)
|
|
729
|
+
self._plot_background_clip_path()
|
|
628
730
|
|
|
629
731
|
self._fit_to_ax()
|
|
630
732
|
|
|
631
733
|
@use_style(LabelStyle, "info_text")
|
|
632
734
|
def info(self, style: LabelStyle = None):
|
|
633
|
-
"""
|
|
735
|
+
"""
|
|
736
|
+
Plots info text in the lower left corner, including date/time and lat/lon.
|
|
634
737
|
|
|
635
738
|
_Only available for ZENITH projections_
|
|
636
739
|
|
|
@@ -643,70 +746,100 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
643
746
|
dt_str = self.dt.strftime("%m/%d/%Y @ %H:%M:%S") + " " + self.dt.tzname()
|
|
644
747
|
info = f"{str(self.lat)}, {str(self.lon)}\n{dt_str}"
|
|
645
748
|
self.ax.text(
|
|
646
|
-
0.
|
|
647
|
-
0.
|
|
749
|
+
0.05,
|
|
750
|
+
0.05,
|
|
648
751
|
info,
|
|
649
752
|
transform=self.ax.transAxes,
|
|
650
753
|
**style.matplot_kwargs(self._size_multiplier * 1.36),
|
|
651
754
|
)
|
|
652
755
|
|
|
653
|
-
def
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
756
|
+
def _plot_background_clip_path(self):
|
|
757
|
+
if self.projection == Projection.ZENITH:
|
|
758
|
+
self._background_clip_path = patches.Circle(
|
|
759
|
+
(0.50, 0.50),
|
|
760
|
+
radius=0.45,
|
|
761
|
+
fill=True,
|
|
762
|
+
facecolor=self.style.background_color.as_hex(),
|
|
763
|
+
# edgecolor=self.style.border_line_color.as_hex(),
|
|
764
|
+
linewidth=0, # 4 * self._size_multiplier,
|
|
765
|
+
zorder=-2_000,
|
|
766
|
+
transform=self.ax.transAxes,
|
|
767
|
+
)
|
|
768
|
+
else:
|
|
769
|
+
# draw patch in axes coords, which are easier to work with
|
|
770
|
+
# in cases like this cause they go from 0...1 in all plots
|
|
771
|
+
self._background_clip_path = patches.Rectangle(
|
|
772
|
+
(0, 0),
|
|
773
|
+
width=1,
|
|
774
|
+
height=1,
|
|
775
|
+
facecolor=self.style.background_color.as_hex(),
|
|
776
|
+
linewidth=0,
|
|
777
|
+
fill=True,
|
|
778
|
+
zorder=-2_000,
|
|
779
|
+
transform=self.ax.transAxes,
|
|
780
|
+
)
|
|
666
781
|
|
|
667
|
-
|
|
668
|
-
(0.5, 0.5),
|
|
669
|
-
radius=0.475,
|
|
670
|
-
fill=True,
|
|
671
|
-
facecolor=self.style.background_color.as_hex(),
|
|
672
|
-
edgecolor=self.style.border_line_color.as_hex(),
|
|
673
|
-
linewidth=8 * self._size_multiplier,
|
|
674
|
-
zorder=-1_000,
|
|
675
|
-
transform=self.ax.transAxes,
|
|
676
|
-
)
|
|
677
|
-
self.ax.add_patch(background_circle)
|
|
782
|
+
self.ax.add_patch(self._background_clip_path)
|
|
678
783
|
|
|
679
|
-
|
|
784
|
+
def border(self, cardinal_direction_labels: list = ["N", "E", "S", "W"]):
|
|
785
|
+
"""
|
|
786
|
+
Plots a border around the map.
|
|
787
|
+
|
|
788
|
+
_Only available for ZENITH projections_
|
|
789
|
+
|
|
790
|
+
Args:
|
|
791
|
+
cardinal_direction_labels: List of labels for cardinal directions on zenith plots. Order matters, labels should be in the order: North, East, South, West.
|
|
792
|
+
"""
|
|
793
|
+
|
|
794
|
+
if not self.projection == Projection.ZENITH:
|
|
795
|
+
raise NotImplementedError("borders only available for zenith projections")
|
|
796
|
+
|
|
797
|
+
if cardinal_direction_labels:
|
|
798
|
+
n, e, s, w = cardinal_direction_labels
|
|
799
|
+
border_font_kwargs = dict(
|
|
800
|
+
fontsize=self.style.border_font_size * self._size_multiplier * 2.26,
|
|
801
|
+
weight=self.style.border_font_weight,
|
|
802
|
+
color=self.style.border_font_color.as_hex(),
|
|
803
|
+
transform=self.ax.transAxes,
|
|
804
|
+
zorder=5000,
|
|
805
|
+
)
|
|
806
|
+
self.ax.text(0.5, 0.986, n, **border_font_kwargs)
|
|
807
|
+
self.ax.text(0.978, 0.5, w, **border_font_kwargs)
|
|
808
|
+
self.ax.text(-0.002, 0.5, e, **border_font_kwargs)
|
|
809
|
+
self.ax.text(0.5, -0.002, s, **border_font_kwargs)
|
|
680
810
|
|
|
681
811
|
border_circle = patches.Circle(
|
|
682
812
|
(0.5, 0.5),
|
|
683
|
-
radius=0.
|
|
813
|
+
radius=0.495,
|
|
684
814
|
fill=False,
|
|
685
815
|
edgecolor=self.style.border_bg_color.as_hex(),
|
|
686
|
-
linewidth=
|
|
687
|
-
zorder=
|
|
816
|
+
linewidth=72 * self._size_multiplier,
|
|
817
|
+
zorder=3_000,
|
|
688
818
|
transform=self.ax.transAxes,
|
|
819
|
+
clip_on=False,
|
|
689
820
|
)
|
|
690
821
|
self.ax.add_patch(border_circle)
|
|
691
822
|
|
|
692
|
-
|
|
823
|
+
inner_border_line_circle = patches.Circle(
|
|
693
824
|
(0.5, 0.5),
|
|
694
|
-
radius=0.
|
|
825
|
+
radius=0.473,
|
|
695
826
|
fill=False,
|
|
696
827
|
edgecolor=self.style.border_line_color.as_hex(),
|
|
697
|
-
linewidth=
|
|
698
|
-
zorder=
|
|
828
|
+
linewidth=4.2 * self._size_multiplier,
|
|
829
|
+
zorder=3_000,
|
|
699
830
|
transform=self.ax.transAxes,
|
|
831
|
+
clip_on=False,
|
|
700
832
|
)
|
|
701
|
-
self.ax.add_patch(
|
|
833
|
+
self.ax.add_patch(inner_border_line_circle)
|
|
702
834
|
|
|
703
|
-
|
|
835
|
+
outer_border_line_circle = patches.Circle(
|
|
704
836
|
(0.5, 0.5),
|
|
705
|
-
radius=0.
|
|
837
|
+
radius=0.52,
|
|
706
838
|
fill=False,
|
|
707
839
|
edgecolor=self.style.border_line_color.as_hex(),
|
|
708
|
-
linewidth=
|
|
709
|
-
zorder=
|
|
840
|
+
linewidth=8.2 * self._size_multiplier,
|
|
841
|
+
zorder=8_000,
|
|
710
842
|
transform=self.ax.transAxes,
|
|
843
|
+
clip_on=False,
|
|
711
844
|
)
|
|
712
|
-
self.ax.add_patch(
|
|
845
|
+
self.ax.add_patch(outer_border_line_circle)
|
starplot/models/__init__.py
CHANGED
starplot/models/base.py
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
from typing import Optional
|
|
1
2
|
from abc import ABC, abstractmethod
|
|
2
3
|
|
|
4
|
+
from skyfield.api import position_of_radec, load_constellation_map
|
|
5
|
+
|
|
3
6
|
from starplot.mixins import CreateMapMixin, CreateOpticMixin
|
|
4
7
|
|
|
8
|
+
constellation_at = load_constellation_map()
|
|
9
|
+
|
|
5
10
|
|
|
6
11
|
class Expression:
|
|
7
12
|
def __init__(self, func=None) -> None:
|
|
@@ -104,10 +109,22 @@ class SkyObject(CreateMapMixin, CreateOpticMixin, metaclass=Meta):
|
|
|
104
109
|
dec: float
|
|
105
110
|
"""Declination, in degrees (-90...90)"""
|
|
106
111
|
|
|
112
|
+
constellation_id: Optional[str] = None
|
|
113
|
+
"""Identifier of the constellation that contains this object. The ID is the three-letter (all lowercase) abbreviation from the International Astronomical Union (IAU)."""
|
|
114
|
+
|
|
107
115
|
def __init__(self, ra: float, dec: float) -> None:
|
|
108
116
|
self.ra = ra
|
|
109
117
|
self.dec = dec
|
|
110
118
|
|
|
119
|
+
pos = position_of_radec(ra, dec)
|
|
120
|
+
self.constellation_id = constellation_at(pos).lower()
|
|
121
|
+
|
|
122
|
+
def constellation(self):
|
|
123
|
+
"""Returns an instance of the [`Constellation`][starplot.models.Constellation] that contains this object"""
|
|
124
|
+
from starplot.models import Constellation
|
|
125
|
+
|
|
126
|
+
return Constellation.get(iau_id=self.constellation_id)
|
|
127
|
+
|
|
111
128
|
|
|
112
129
|
class SkyObjectManager(ABC):
|
|
113
130
|
@abstractmethod
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from starplot.models.base import SkyObject, SkyObjectManager
|
|
2
|
+
from starplot.data import constellations
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ConstellationManager(SkyObjectManager):
|
|
6
|
+
@classmethod
|
|
7
|
+
def all(cls):
|
|
8
|
+
for iau_id in constellations.iterator():
|
|
9
|
+
name, ra, dec = constellations.get(iau_id)
|
|
10
|
+
yield Constellation(
|
|
11
|
+
ra=ra, dec=dec, iau_id=iau_id, name=name.replace("\n", " ")
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Constellation(SkyObject):
|
|
16
|
+
"""
|
|
17
|
+
Constellation model.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
_manager = ConstellationManager
|
|
21
|
+
|
|
22
|
+
iau_id: str = None
|
|
23
|
+
"""International Astronomical Union (IAU) three-letter designation, all lowercase"""
|
|
24
|
+
|
|
25
|
+
name: str = None
|
|
26
|
+
"""Name"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
ra: float,
|
|
31
|
+
dec: float,
|
|
32
|
+
iau_id: str,
|
|
33
|
+
name: str = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
super().__init__(ra, dec)
|
|
36
|
+
self.iau_id = iau_id.lower()
|
|
37
|
+
self.name = name
|
|
38
|
+
|
|
39
|
+
def __repr__(self) -> str:
|
|
40
|
+
return f"Constellation(iau_id={self.iau_id}, name={self.name}, ra={self.ra}, dec={self.dec})"
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def get(**kwargs) -> "Constellation":
|
|
44
|
+
"""
|
|
45
|
+
Get a Constellation, by matching its attributes.
|
|
46
|
+
|
|
47
|
+
Example: `hercules = Constellation.get(name="Hercules")`
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
**kwargs: Attributes on the constellation you want to match
|
|
51
|
+
|
|
52
|
+
Raises: `ValueError` if more than one constellation is matched
|
|
53
|
+
"""
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def find(where: list) -> list["Constellation"]:
|
|
58
|
+
"""
|
|
59
|
+
Find Constellations
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
where: A list of expressions that determine which constellations to find. See [Selecting Objects](/reference-selecting-objects/) for details.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
List of Constellations that match all `where` expressions
|
|
66
|
+
|
|
67
|
+
"""
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
def constellation(self):
|
|
71
|
+
"""Not applicable to Constellation model, raises `NotImplementedError`"""
|
|
72
|
+
raise NotImplementedError()
|
starplot/models/star.py
CHANGED
|
@@ -40,6 +40,9 @@ class Star(SkyObject):
|
|
|
40
40
|
hip: Optional[int] = None
|
|
41
41
|
"""Hipparcos Catalog ID, if available"""
|
|
42
42
|
|
|
43
|
+
tyc: Optional[str] = None
|
|
44
|
+
"""Tycho ID, if available"""
|
|
45
|
+
|
|
43
46
|
name: Optional[str] = None
|
|
44
47
|
"""Name, if available"""
|
|
45
48
|
|
|
@@ -51,12 +54,17 @@ class Star(SkyObject):
|
|
|
51
54
|
bv: float = None,
|
|
52
55
|
hip: int = None,
|
|
53
56
|
name: str = None,
|
|
57
|
+
tyc: str = None,
|
|
54
58
|
) -> None:
|
|
55
59
|
super().__init__(ra, dec)
|
|
56
60
|
self.magnitude = magnitude
|
|
57
61
|
self.bv = bv
|
|
58
62
|
self.hip = hip
|
|
59
63
|
self.name = name
|
|
64
|
+
self.tyc = tyc
|
|
65
|
+
|
|
66
|
+
def __repr__(self) -> str:
|
|
67
|
+
return f"Star(hip={self.hip}, tyc={self.tyc}, magnitude={self.magnitude}, ra={self.ra}, dec={self.dec})"
|
|
60
68
|
|
|
61
69
|
@classmethod
|
|
62
70
|
def get(**kwargs) -> "Star":
|
starplot/optic.py
CHANGED
|
@@ -189,7 +189,7 @@ class OpticPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
189
189
|
def stars(
|
|
190
190
|
self,
|
|
191
191
|
mag: float = 6.0,
|
|
192
|
-
catalog: StarCatalog = StarCatalog.
|
|
192
|
+
catalog: StarCatalog = StarCatalog.BIG_SKY_MAG11,
|
|
193
193
|
style: ObjectStyle = None,
|
|
194
194
|
rasterize: bool = False,
|
|
195
195
|
size_fn: Callable[[Star], float] = callables.size_by_magnitude_for_optic,
|
|
@@ -208,7 +208,7 @@ class OpticPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
208
208
|
|
|
209
209
|
Args:
|
|
210
210
|
mag: Limiting magnitude of stars to plot
|
|
211
|
-
catalog: The catalog of stars to use
|
|
211
|
+
catalog: The catalog of stars to use
|
|
212
212
|
style: If `None`, then the plot's style for stars will be used
|
|
213
213
|
rasterize: If True, then the stars will be rasterized when plotted, which can speed up exporting to SVG and reduce the file size but with a loss of image quality
|
|
214
214
|
size_fn: Callable for calculating the marker size of each star. If `None`, then the marker style's size will be used.
|
starplot/plotters/stars.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from typing import Callable, Mapping
|
|
2
|
+
from functools import cache
|
|
2
3
|
|
|
3
4
|
import numpy as np
|
|
4
5
|
|
|
@@ -12,6 +13,7 @@ from starplot.styles import ObjectStyle, LabelStyle, use_style
|
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class StarPlotterMixin:
|
|
16
|
+
@cache
|
|
15
17
|
def _load_stars(self, catalog, limiting_magnitude=None):
|
|
16
18
|
stardata = stars.load(catalog)
|
|
17
19
|
|
|
@@ -125,7 +127,7 @@ class StarPlotterMixin:
|
|
|
125
127
|
|
|
126
128
|
Args:
|
|
127
129
|
mag: Limiting magnitude of stars to plot. For more control of what stars to plot, use the `where` kwarg. **Note:** if you pass `mag` and `where` then `mag` will be ignored
|
|
128
|
-
catalog: The catalog of stars to use: "hipparcos" or "
|
|
130
|
+
catalog: The catalog of stars to use: "hipparcos", "big-sky-mag11", or "big-sky" -- see [`StarCatalog`](/reference-data/#starplot.data.stars.StarCatalog) for details
|
|
129
131
|
style: If `None`, then the plot's style for stars will be used
|
|
130
132
|
rasterize: If True, then the stars will be rasterized when plotted, which can speed up exporting to SVG and reduce the file size but with a loss of image quality
|
|
131
133
|
size_fn: Callable for calculating the marker size of each star. If `None`, then the marker style's size will be used.
|
|
@@ -140,9 +142,12 @@ class StarPlotterMixin:
|
|
|
140
142
|
self.logger.debug("Plotting stars...")
|
|
141
143
|
|
|
142
144
|
# fallback to style if callables are None
|
|
145
|
+
color_hex = (
|
|
146
|
+
style.marker.color.as_hex()
|
|
147
|
+
) # calculate color hex once here to avoid repeated calls in color_fn()
|
|
143
148
|
size_fn = size_fn or (lambda d: style.marker.size)
|
|
144
149
|
alpha_fn = alpha_fn or (lambda d: style.marker.alpha)
|
|
145
|
-
color_fn = color_fn or (lambda d:
|
|
150
|
+
color_fn = color_fn or (lambda d: color_hex)
|
|
146
151
|
|
|
147
152
|
where = where or []
|
|
148
153
|
|
|
@@ -171,9 +176,22 @@ class StarPlotterMixin:
|
|
|
171
176
|
for star in nearby_stars_df.itertuples():
|
|
172
177
|
m = star.magnitude
|
|
173
178
|
ra, dec = star.ra, star.dec
|
|
174
|
-
hip_id = star.Index
|
|
175
179
|
|
|
176
|
-
|
|
180
|
+
if catalog == StarCatalog.HIPPARCOS:
|
|
181
|
+
hip_id = star.Index
|
|
182
|
+
tyc_id = None
|
|
183
|
+
else:
|
|
184
|
+
hip_id = star.hip
|
|
185
|
+
tyc_id = star.Index
|
|
186
|
+
|
|
187
|
+
obj = Star(
|
|
188
|
+
ra=ra / 15,
|
|
189
|
+
dec=dec,
|
|
190
|
+
magnitude=m,
|
|
191
|
+
bv=star.bv,
|
|
192
|
+
hip=hip_id,
|
|
193
|
+
tyc=tyc_id,
|
|
194
|
+
)
|
|
177
195
|
|
|
178
196
|
if np.isfinite(hip_id):
|
|
179
197
|
obj.hip = hip_id
|
|
@@ -205,7 +223,9 @@ class StarPlotterMixin:
|
|
|
205
223
|
colors,
|
|
206
224
|
style=style,
|
|
207
225
|
zorder=style.marker.zorder,
|
|
208
|
-
edgecolors=
|
|
226
|
+
edgecolors=style.marker.edge_color.as_hex()
|
|
227
|
+
if style.marker.edge_color
|
|
228
|
+
else "none",
|
|
209
229
|
rasterized=rasterize,
|
|
210
230
|
)
|
|
211
231
|
|
starplot/projections.py
CHANGED
|
@@ -28,6 +28,12 @@ class Projection(str, Enum):
|
|
|
28
28
|
**This is a _perspective_ projection, so it requires the following `kwargs` when creating the plot: `lat`, `lon`, and `dt`**. _The perspective of the globe will be based on these values._
|
|
29
29
|
"""
|
|
30
30
|
|
|
31
|
+
ROBINSON = "robinson"
|
|
32
|
+
"""Good for showing the entire celestial sphere in one plot"""
|
|
33
|
+
|
|
34
|
+
LAMBERT_AZ_EQ_AREA = "lambert_az_eq_area"
|
|
35
|
+
"""Lambert Azimuthal Equal-Area projection - accurately shows area, but distorts angles."""
|
|
36
|
+
|
|
31
37
|
STEREOGRAPHIC = "stereographic"
|
|
32
38
|
"""
|
|
33
39
|
Similar to the North/South Stereographic projection, but this version is location-dependent.
|
|
@@ -47,10 +53,12 @@ class Projection(str, Enum):
|
|
|
47
53
|
projections = {
|
|
48
54
|
Projection.STEREO_NORTH: ccrs.NorthPolarStereo,
|
|
49
55
|
Projection.STEREO_SOUTH: ccrs.SouthPolarStereo,
|
|
56
|
+
Projection.LAMBERT_AZ_EQ_AREA: ccrs.LambertAzimuthalEqualArea,
|
|
50
57
|
Projection.MERCATOR: ccrs.Mercator,
|
|
51
58
|
Projection.MOLLWEIDE: ccrs.Mollweide,
|
|
52
59
|
Projection.MILLER: ccrs.Miller,
|
|
53
60
|
Projection.ORTHOGRAPHIC: ccrs.Orthographic,
|
|
61
|
+
Projection.ROBINSON: ccrs.Robinson,
|
|
54
62
|
Projection.STEREOGRAPHIC: ccrs.Stereographic,
|
|
55
63
|
Projection.ZENITH: ccrs.Stereographic,
|
|
56
64
|
}
|
|
@@ -64,4 +72,7 @@ class Projection(str, Enum):
|
|
|
64
72
|
central_longitude=kwargs["lon"], central_latitude=kwargs["lat"]
|
|
65
73
|
)
|
|
66
74
|
else:
|
|
75
|
+
if kwargs.get("center_lat") is not None:
|
|
76
|
+
kwargs["central_latitude"] = kwargs.pop("center_lat")
|
|
77
|
+
|
|
67
78
|
return proj_class(center_lon, **kwargs)
|