starplot 0.11.4__py2.py3-none-any.whl → 0.12.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.
Potentially problematic release.
This version of starplot might be problematic. Click here for more details.
- starplot/__init__.py +1 -1
- starplot/base.py +21 -1
- starplot/data/constellations.py +16 -1
- starplot/data/dsos.py +1 -1
- starplot/data/library/constellations.gpkg +0 -0
- starplot/data/library/ongc.gpkg.zip +0 -0
- starplot/data/prep/__init__.py +0 -0
- starplot/data/prep/constellations.py +108 -0
- starplot/data/prep/dsos.py +299 -0
- starplot/data/prep/utils.py +16 -0
- starplot/map.py +90 -84
- starplot/models/__init__.py +1 -1
- starplot/models/base.py +9 -3
- starplot/models/constellation.py +27 -5
- starplot/models/dso.py +47 -1
- starplot/models/geometry.py +44 -0
- starplot/models/moon.py +8 -0
- starplot/models/objects.py +5 -1
- starplot/models/planet.py +14 -1
- starplot/models/star.py +47 -7
- starplot/models/sun.py +14 -1
- starplot/optic.py +11 -4
- starplot/plotters/dsos.py +58 -28
- starplot/plotters/stars.py +15 -29
- starplot/styles/base.py +98 -52
- starplot/styles/ext/antique.yml +29 -1
- starplot/styles/ext/blue_dark.yml +20 -2
- starplot/styles/ext/blue_light.yml +29 -1
- starplot/styles/ext/blue_medium.yml +30 -1
- starplot/styles/ext/cb_wong.yml +28 -1
- starplot/styles/ext/color_print.yml +3 -0
- starplot/styles/ext/grayscale.yml +18 -1
- starplot/styles/ext/grayscale_dark.yml +20 -1
- starplot/styles/ext/nord.yml +33 -7
- starplot/styles/markers.py +107 -0
- starplot/utils.py +1 -1
- {starplot-0.11.4.dist-info → starplot-0.12.1.dist-info}/METADATA +4 -13
- starplot-0.12.1.dist-info/RECORD +67 -0
- starplot/data/library/de440s.bsp +0 -0
- starplot-0.11.4.dist-info/RECORD +0 -62
- {starplot-0.11.4.dist-info → starplot-0.12.1.dist-info}/LICENSE +0 -0
- {starplot-0.11.4.dist-info → starplot-0.12.1.dist-info}/WHEEL +0 -0
starplot/map.py
CHANGED
|
@@ -7,7 +7,7 @@ from cartopy import crs as ccrs
|
|
|
7
7
|
from matplotlib import pyplot as plt
|
|
8
8
|
from matplotlib import path, patches, ticker
|
|
9
9
|
from matplotlib.ticker import FuncFormatter, FixedLocator
|
|
10
|
-
from shapely import LineString, MultiLineString
|
|
10
|
+
from shapely import LineString, MultiLineString, Polygon
|
|
11
11
|
from shapely.ops import unary_union
|
|
12
12
|
from skyfield.api import Star as SkyfieldStar, wgs84
|
|
13
13
|
import geopandas as gpd
|
|
@@ -18,6 +18,7 @@ from starplot.base import BasePlot
|
|
|
18
18
|
from starplot.data import DataFiles, constellations as condata, stars
|
|
19
19
|
from starplot.data.constellations import CONSTELLATIONS_FULL_NAMES
|
|
20
20
|
from starplot.mixins import ExtentMaskMixin
|
|
21
|
+
from starplot.models.constellation import from_tuple as constellation_from_tuple
|
|
21
22
|
from starplot.plotters import StarPlotterMixin, DsoPlotterMixin
|
|
22
23
|
from starplot.projections import Projection
|
|
23
24
|
from starplot.styles import (
|
|
@@ -58,6 +59,7 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
58
59
|
style: Styling for the plot (colors, sizes, fonts, etc)
|
|
59
60
|
resolution: Size (in pixels) of largest dimension of the map
|
|
60
61
|
hide_colliding_labels: If True, then labels will not be plotted if they collide with another existing label
|
|
62
|
+
clip_path: An optional Shapely Polygon that specifies the clip path of the plot -- only objects inside the polygon will be plotted. If `None` (the default), then the clip path will be the extent of the map you specified with the RA/DEC parameters.
|
|
61
63
|
|
|
62
64
|
Returns:
|
|
63
65
|
MapPlot: A new instance of a MapPlot
|
|
@@ -78,6 +80,7 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
78
80
|
style: PlotStyle = DEFAULT_MAP_STYLE,
|
|
79
81
|
resolution: int = 2048,
|
|
80
82
|
hide_colliding_labels: bool = True,
|
|
83
|
+
clip_path: Polygon = None,
|
|
81
84
|
*args,
|
|
82
85
|
**kwargs,
|
|
83
86
|
) -> "MapPlot":
|
|
@@ -106,6 +109,7 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
106
109
|
self.dec_max = dec_max
|
|
107
110
|
self.lat = lat
|
|
108
111
|
self.lon = lon
|
|
112
|
+
self.clip_path = clip_path
|
|
109
113
|
|
|
110
114
|
if self.projection in [
|
|
111
115
|
Projection.ORTHOGRAPHIC,
|
|
@@ -259,6 +263,8 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
259
263
|
list(x),
|
|
260
264
|
list(y),
|
|
261
265
|
transform=self._plate_carree,
|
|
266
|
+
clip_on=True,
|
|
267
|
+
clip_path=self._background_clip_path,
|
|
262
268
|
**style_kwargs,
|
|
263
269
|
)
|
|
264
270
|
|
|
@@ -342,14 +348,20 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
342
348
|
self,
|
|
343
349
|
style: PathStyle = None,
|
|
344
350
|
labels: dict[str, str] = CONSTELLATIONS_FULL_NAMES,
|
|
351
|
+
where: list = None,
|
|
345
352
|
):
|
|
346
353
|
"""Plots the constellation lines and/or labels
|
|
347
354
|
|
|
348
355
|
Args:
|
|
349
356
|
style: Styling of the constellations. If None, then the plot's style (specified when creating the plot) will be used
|
|
350
357
|
labels: A dictionary where the keys are each constellation's 3-letter abbreviation, and the values are how the constellation will be labeled on the plot.
|
|
358
|
+
where: A list of expressions that determine which constellations to plot. See [Selecting Objects](/reference-selecting-objects/) for details.
|
|
351
359
|
"""
|
|
360
|
+
self.logger.debug("Plotting constellations...")
|
|
361
|
+
|
|
352
362
|
labels = labels or {}
|
|
363
|
+
where = where or []
|
|
364
|
+
|
|
353
365
|
constellations_gdf = gpd.read_file(
|
|
354
366
|
DataFiles.CONSTELLATIONS.value,
|
|
355
367
|
engine="pyogrio",
|
|
@@ -369,8 +381,14 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
369
381
|
conline_hips = condata.lines()
|
|
370
382
|
style_kwargs = style.line.matplot_kwargs(size_multiplier=self._size_multiplier)
|
|
371
383
|
|
|
372
|
-
for
|
|
373
|
-
|
|
384
|
+
for c in constellations_gdf.itertuples():
|
|
385
|
+
obj = constellation_from_tuple(c)
|
|
386
|
+
|
|
387
|
+
if not all([e.evaluate(obj) for e in where]):
|
|
388
|
+
continue
|
|
389
|
+
|
|
390
|
+
hiplines = conline_hips[c.iau_id]
|
|
391
|
+
inbounds = False
|
|
374
392
|
|
|
375
393
|
for s1_hip, s2_hip in hiplines:
|
|
376
394
|
s1 = stars_df.loc[s1_hip]
|
|
@@ -388,6 +406,9 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
388
406
|
elif s2_ra - s1_ra > 60:
|
|
389
407
|
s1_ra += 360
|
|
390
408
|
|
|
409
|
+
if self.in_bounds(s1_ra / 15, s1_dec):
|
|
410
|
+
inbounds = True
|
|
411
|
+
|
|
391
412
|
s1_ra *= -1
|
|
392
413
|
s2_ra *= -1
|
|
393
414
|
|
|
@@ -400,8 +421,13 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
400
421
|
[s1_dec, s2_dec],
|
|
401
422
|
transform=transform,
|
|
402
423
|
**style_kwargs,
|
|
424
|
+
clip_on=True,
|
|
425
|
+
clip_path=self._background_clip_path,
|
|
403
426
|
)
|
|
404
427
|
|
|
428
|
+
if inbounds:
|
|
429
|
+
self._objects.constellations.append(obj)
|
|
430
|
+
|
|
405
431
|
self._plot_constellation_labels(style.label, labels)
|
|
406
432
|
|
|
407
433
|
def _plot_constellation_labels(
|
|
@@ -425,18 +451,28 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
425
451
|
"""
|
|
426
452
|
mw = self._read_geo_package(DataFiles.MILKY_WAY.value)
|
|
427
453
|
|
|
428
|
-
if
|
|
429
|
-
|
|
430
|
-
style_kwargs.pop("fill", None)
|
|
454
|
+
if mw.empty:
|
|
455
|
+
return
|
|
431
456
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
457
|
+
def _prepare_polygon(p):
|
|
458
|
+
points = list(zip(*p.boundary.coords.xy))
|
|
459
|
+
# convert lon to RA and reverse so the coordinates are counterclockwise order
|
|
460
|
+
return [(lon_to_ra(lon) * 15, dec) for lon, dec in reversed(points)]
|
|
435
461
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
462
|
+
# create union of all Milky Way patches
|
|
463
|
+
gs = mw.geometry.to_crs(self._plate_carree)
|
|
464
|
+
mw_union = gs.buffer(0.1).unary_union.buffer(-0.1)
|
|
465
|
+
polygons = []
|
|
466
|
+
|
|
467
|
+
if mw_union.geom_type == "MultiPolygon":
|
|
468
|
+
polygons.extend([_prepare_polygon(polygon) for polygon in mw_union.geoms])
|
|
469
|
+
else:
|
|
470
|
+
polygons.append(_prepare_polygon(mw_union))
|
|
471
|
+
|
|
472
|
+
for polygon_points in polygons:
|
|
473
|
+
self._polygon(
|
|
474
|
+
polygon_points,
|
|
475
|
+
style=style,
|
|
440
476
|
)
|
|
441
477
|
|
|
442
478
|
@use_style(ObjectStyle, "zenith")
|
|
@@ -616,8 +652,9 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
616
652
|
return dec_formatter_fn(x)
|
|
617
653
|
|
|
618
654
|
ra_locations = ra_locations or [x for x in range(24)]
|
|
619
|
-
dec_locations = dec_locations or [d for d in range(-
|
|
655
|
+
dec_locations = dec_locations or [d for d in range(-80, 90, 10)]
|
|
620
656
|
|
|
657
|
+
line_style_kwargs = style.line.matplot_kwargs()
|
|
621
658
|
gridlines = self.ax.gridlines(
|
|
622
659
|
draw_labels=labels,
|
|
623
660
|
x_inline=False,
|
|
@@ -625,23 +662,36 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
625
662
|
rotate_labels=False,
|
|
626
663
|
xpadding=12,
|
|
627
664
|
ypadding=12,
|
|
628
|
-
|
|
665
|
+
clip_on=True,
|
|
666
|
+
clip_path=self._background_clip_path,
|
|
667
|
+
**line_style_kwargs,
|
|
629
668
|
)
|
|
630
669
|
|
|
631
670
|
if labels:
|
|
632
671
|
self._axis_labels = True
|
|
633
672
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
673
|
+
label_style_kwargs = style.label.matplot_kwargs()
|
|
674
|
+
label_style_kwargs.pop("va")
|
|
675
|
+
label_style_kwargs.pop("ha")
|
|
676
|
+
|
|
677
|
+
if self.dec_max > 75 or self.dec_min < -75:
|
|
678
|
+
# if the extent is near the poles, then plot the RA gridlines again
|
|
679
|
+
# because cartopy does not extend lines to poles
|
|
680
|
+
for ra in ra_locations:
|
|
681
|
+
self.ax.plot(
|
|
682
|
+
(ra * 15, ra * 15),
|
|
683
|
+
(-90, 90),
|
|
684
|
+
**line_style_kwargs,
|
|
685
|
+
**self._plot_kwargs(),
|
|
686
|
+
)
|
|
637
687
|
|
|
638
688
|
gridlines.xlocator = FixedLocator([ra_to_lon(r) for r in ra_locations])
|
|
639
689
|
gridlines.xformatter = FuncFormatter(ra_formatter)
|
|
640
|
-
gridlines.xlabel_style =
|
|
690
|
+
gridlines.xlabel_style = label_style_kwargs
|
|
641
691
|
|
|
642
692
|
gridlines.ylocator = FixedLocator(dec_locations)
|
|
643
693
|
gridlines.yformatter = FuncFormatter(dec_formatter)
|
|
644
|
-
gridlines.ylabel_style =
|
|
694
|
+
gridlines.ylabel_style = label_style_kwargs
|
|
645
695
|
|
|
646
696
|
if tick_marks:
|
|
647
697
|
self._tick_marks(style, ra_tick_locations, dec_tick_locations)
|
|
@@ -754,7 +804,26 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
754
804
|
)
|
|
755
805
|
|
|
756
806
|
def _plot_background_clip_path(self):
|
|
757
|
-
|
|
807
|
+
def to_axes(points):
|
|
808
|
+
ax_points = []
|
|
809
|
+
|
|
810
|
+
for ra, dec in points:
|
|
811
|
+
x, y = self._proj.transform_point(ra * 15, dec, self._crs)
|
|
812
|
+
data_to_axes = self.ax.transData + self.ax.transAxes.inverted()
|
|
813
|
+
x_axes, y_axes = data_to_axes.transform((x, y))
|
|
814
|
+
ax_points.append([x_axes, y_axes])
|
|
815
|
+
return ax_points
|
|
816
|
+
|
|
817
|
+
if self.clip_path is not None:
|
|
818
|
+
points = list(zip(*self.clip_path.exterior.coords.xy))
|
|
819
|
+
self._background_clip_path = patches.Polygon(
|
|
820
|
+
to_axes(points),
|
|
821
|
+
facecolor=self.style.background_color.as_hex(),
|
|
822
|
+
fill=True,
|
|
823
|
+
zorder=-2_000,
|
|
824
|
+
transform=self.ax.transAxes,
|
|
825
|
+
)
|
|
826
|
+
elif self.projection == Projection.ZENITH:
|
|
758
827
|
self._background_clip_path = patches.Circle(
|
|
759
828
|
(0.50, 0.50),
|
|
760
829
|
radius=0.45,
|
|
@@ -780,66 +849,3 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
780
849
|
)
|
|
781
850
|
|
|
782
851
|
self.ax.add_patch(self._background_clip_path)
|
|
783
|
-
|
|
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)
|
|
810
|
-
|
|
811
|
-
border_circle = patches.Circle(
|
|
812
|
-
(0.5, 0.5),
|
|
813
|
-
radius=0.495,
|
|
814
|
-
fill=False,
|
|
815
|
-
edgecolor=self.style.border_bg_color.as_hex(),
|
|
816
|
-
linewidth=72 * self._size_multiplier,
|
|
817
|
-
zorder=3_000,
|
|
818
|
-
transform=self.ax.transAxes,
|
|
819
|
-
clip_on=False,
|
|
820
|
-
)
|
|
821
|
-
self.ax.add_patch(border_circle)
|
|
822
|
-
|
|
823
|
-
inner_border_line_circle = patches.Circle(
|
|
824
|
-
(0.5, 0.5),
|
|
825
|
-
radius=0.473,
|
|
826
|
-
fill=False,
|
|
827
|
-
edgecolor=self.style.border_line_color.as_hex(),
|
|
828
|
-
linewidth=4.2 * self._size_multiplier,
|
|
829
|
-
zorder=3_000,
|
|
830
|
-
transform=self.ax.transAxes,
|
|
831
|
-
clip_on=False,
|
|
832
|
-
)
|
|
833
|
-
self.ax.add_patch(inner_border_line_circle)
|
|
834
|
-
|
|
835
|
-
outer_border_line_circle = patches.Circle(
|
|
836
|
-
(0.5, 0.5),
|
|
837
|
-
radius=0.52,
|
|
838
|
-
fill=False,
|
|
839
|
-
edgecolor=self.style.border_line_color.as_hex(),
|
|
840
|
-
linewidth=8.2 * self._size_multiplier,
|
|
841
|
-
zorder=8_000,
|
|
842
|
-
transform=self.ax.transAxes,
|
|
843
|
-
clip_on=False,
|
|
844
|
-
)
|
|
845
|
-
self.ax.add_patch(outer_border_line_circle)
|
starplot/models/__init__.py
CHANGED
starplot/models/base.py
CHANGED
|
@@ -79,6 +79,10 @@ class Term:
|
|
|
79
79
|
"""Returns `True` if the field value is NOT `None`"""
|
|
80
80
|
return Expression(func=lambda c: getattr(c, self.attr) is not None)
|
|
81
81
|
|
|
82
|
+
def intersects(self, other):
|
|
83
|
+
"""Returns `True` if the field's value intersects `other`. Only available for geometry-type fields."""
|
|
84
|
+
return Expression(func=lambda c: getattr(c, self.attr).intersects(other))
|
|
85
|
+
|
|
82
86
|
|
|
83
87
|
class Meta(type):
|
|
84
88
|
managers = {}
|
|
@@ -132,14 +136,16 @@ class SkyObjectManager(ABC):
|
|
|
132
136
|
raise NotImplementedError
|
|
133
137
|
|
|
134
138
|
@classmethod
|
|
135
|
-
def find(cls, where):
|
|
136
|
-
|
|
139
|
+
def find(cls, where, **kwargs):
|
|
140
|
+
all_objects = kwargs.pop("all_objects", None) or cls.all()
|
|
141
|
+
return [s for s in all_objects if all([e.evaluate(s) for e in where])]
|
|
137
142
|
|
|
138
143
|
@classmethod
|
|
139
144
|
def get(cls, **kwargs):
|
|
145
|
+
all_objects = kwargs.pop("all_objects", None) or cls.all()
|
|
140
146
|
matches = [
|
|
141
147
|
s
|
|
142
|
-
for s in
|
|
148
|
+
for s in all_objects
|
|
143
149
|
if all([getattr(s, kw) == value for kw, value in kwargs.items()])
|
|
144
150
|
]
|
|
145
151
|
|
starplot/models/constellation.py
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
+
from shapely import Polygon
|
|
2
|
+
|
|
1
3
|
from starplot.models.base import SkyObject, SkyObjectManager
|
|
4
|
+
from starplot.models.geometry import to_24h
|
|
2
5
|
from starplot.data import constellations
|
|
3
6
|
|
|
4
7
|
|
|
5
8
|
class ConstellationManager(SkyObjectManager):
|
|
6
9
|
@classmethod
|
|
7
10
|
def all(cls):
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
yield
|
|
11
|
-
ra=ra, dec=dec, iau_id=iau_id, name=name.replace("\n", " ")
|
|
12
|
-
)
|
|
11
|
+
all_constellations = constellations.load()
|
|
12
|
+
for constellation in all_constellations.itertuples():
|
|
13
|
+
yield from_tuple(constellation)
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class Constellation(SkyObject):
|
|
@@ -25,16 +26,21 @@ class Constellation(SkyObject):
|
|
|
25
26
|
name: str = None
|
|
26
27
|
"""Name"""
|
|
27
28
|
|
|
29
|
+
boundary: Polygon = None
|
|
30
|
+
"""Shapely Polygon of the constellation's boundary. Right ascension coordinates are in 24H format."""
|
|
31
|
+
|
|
28
32
|
def __init__(
|
|
29
33
|
self,
|
|
30
34
|
ra: float,
|
|
31
35
|
dec: float,
|
|
32
36
|
iau_id: str,
|
|
33
37
|
name: str = None,
|
|
38
|
+
boundary: Polygon = None,
|
|
34
39
|
) -> None:
|
|
35
40
|
super().__init__(ra, dec)
|
|
36
41
|
self.iau_id = iau_id.lower()
|
|
37
42
|
self.name = name
|
|
43
|
+
self.boundary = boundary
|
|
38
44
|
|
|
39
45
|
def __repr__(self) -> str:
|
|
40
46
|
return f"Constellation(iau_id={self.iau_id}, name={self.name}, ra={self.ra}, dec={self.dec})"
|
|
@@ -70,3 +76,19 @@ class Constellation(SkyObject):
|
|
|
70
76
|
def constellation(self):
|
|
71
77
|
"""Not applicable to Constellation model, raises `NotImplementedError`"""
|
|
72
78
|
raise NotImplementedError()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def from_tuple(c: tuple) -> Constellation:
|
|
82
|
+
geometry = c.geometry
|
|
83
|
+
if len(c.geometry.geoms) == 1:
|
|
84
|
+
geometry = c.geometry.geoms[0]
|
|
85
|
+
|
|
86
|
+
geometry = to_24h(geometry)
|
|
87
|
+
|
|
88
|
+
return Constellation(
|
|
89
|
+
ra=c.center_ra / 15,
|
|
90
|
+
dec=c.center_dec,
|
|
91
|
+
iau_id=c.iau_id,
|
|
92
|
+
name=c.name,
|
|
93
|
+
boundary=geometry,
|
|
94
|
+
)
|
starplot/models/dso.py
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
from typing import Optional
|
|
1
|
+
from typing import Optional, Union
|
|
2
|
+
|
|
3
|
+
from shapely.geometry import Polygon, MultiPolygon
|
|
2
4
|
|
|
3
5
|
from starplot.data.dsos import DsoType, load_ongc, ONGC_TYPE_MAP
|
|
4
6
|
from starplot.mixins import CreateMapMixin, CreateOpticMixin
|
|
5
7
|
from starplot.models.base import SkyObject, SkyObjectManager
|
|
8
|
+
from starplot.models.geometry import to_24h
|
|
9
|
+
from starplot import geod
|
|
6
10
|
|
|
7
11
|
|
|
8
12
|
class DsoManager(SkyObjectManager):
|
|
@@ -59,6 +63,9 @@ class DSO(SkyObject, CreateMapMixin, CreateOpticMixin):
|
|
|
59
63
|
Index Catalogue (IC) identifier. *Note that this field is a string, to support objects like '4974 NED01'.*
|
|
60
64
|
"""
|
|
61
65
|
|
|
66
|
+
geometry: Union[Polygon, MultiPolygon] = None
|
|
67
|
+
"""Shapely Polygon of the DSO's extent. Right ascension coordinates are in 24H format."""
|
|
68
|
+
|
|
62
69
|
def __init__(
|
|
63
70
|
self,
|
|
64
71
|
ra: float,
|
|
@@ -73,6 +80,7 @@ class DSO(SkyObject, CreateMapMixin, CreateOpticMixin):
|
|
|
73
80
|
m: str = None,
|
|
74
81
|
ngc: str = None,
|
|
75
82
|
ic: str = None,
|
|
83
|
+
geometry: Union[Polygon, MultiPolygon] = None,
|
|
76
84
|
) -> None:
|
|
77
85
|
super().__init__(ra, dec)
|
|
78
86
|
self.name = name
|
|
@@ -85,6 +93,7 @@ class DSO(SkyObject, CreateMapMixin, CreateOpticMixin):
|
|
|
85
93
|
self.m = m
|
|
86
94
|
self.ngc = ngc
|
|
87
95
|
self.ic = ic
|
|
96
|
+
self.geometry = geometry
|
|
88
97
|
|
|
89
98
|
def __repr__(self) -> str:
|
|
90
99
|
return f"DSO(name={self.name}, magnitude={self.magnitude})"
|
|
@@ -118,9 +127,45 @@ class DSO(SkyObject, CreateMapMixin, CreateOpticMixin):
|
|
|
118
127
|
pass
|
|
119
128
|
|
|
120
129
|
|
|
130
|
+
def create_ellipse(d):
|
|
131
|
+
maj_ax, min_ax, angle = d.maj_ax, d.min_ax, d.angle
|
|
132
|
+
|
|
133
|
+
if maj_ax is None:
|
|
134
|
+
return d.geometry
|
|
135
|
+
|
|
136
|
+
if angle is None:
|
|
137
|
+
angle = 0
|
|
138
|
+
|
|
139
|
+
maj_ax_degrees = (maj_ax / 60) / 2
|
|
140
|
+
|
|
141
|
+
if not min_ax:
|
|
142
|
+
min_ax_degrees = maj_ax_degrees
|
|
143
|
+
else:
|
|
144
|
+
min_ax_degrees = (min_ax / 60) / 2
|
|
145
|
+
|
|
146
|
+
points = geod.ellipse(
|
|
147
|
+
(d.ra_degrees / 15, d.dec_degrees),
|
|
148
|
+
min_ax_degrees * 2,
|
|
149
|
+
maj_ax_degrees * 2,
|
|
150
|
+
angle,
|
|
151
|
+
num_pts=100,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# points = [geod.to_radec(p) for p in points]
|
|
155
|
+
points = [(round(ra, 4), round(dec, 4)) for ra, dec in points]
|
|
156
|
+
return Polygon(points)
|
|
157
|
+
|
|
158
|
+
|
|
121
159
|
def from_tuple(d: tuple) -> DSO:
|
|
122
160
|
magnitude = d.mag_v or d.mag_b or None
|
|
123
161
|
magnitude = float(magnitude) if magnitude else None
|
|
162
|
+
geometry = d.geometry
|
|
163
|
+
|
|
164
|
+
if str(geometry.geom_type) not in ["Polygon", "MultiPolygon"]:
|
|
165
|
+
geometry = create_ellipse(d)
|
|
166
|
+
|
|
167
|
+
geometry = to_24h(geometry)
|
|
168
|
+
|
|
124
169
|
return DSO(
|
|
125
170
|
name=d.name,
|
|
126
171
|
ra=d.ra_degrees / 15,
|
|
@@ -134,4 +179,5 @@ def from_tuple(d: tuple) -> DSO:
|
|
|
134
179
|
m=d.m,
|
|
135
180
|
ngc=d.ngc,
|
|
136
181
|
ic=d.ic,
|
|
182
|
+
geometry=geometry,
|
|
137
183
|
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
3
|
+
from shapely.geometry import Point, Polygon, MultiPolygon
|
|
4
|
+
|
|
5
|
+
from starplot import geod, utils
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def circle(center, diameter_degrees):
|
|
9
|
+
points = geod.ellipse(
|
|
10
|
+
center,
|
|
11
|
+
diameter_degrees,
|
|
12
|
+
diameter_degrees,
|
|
13
|
+
angle=0,
|
|
14
|
+
num_pts=100,
|
|
15
|
+
)
|
|
16
|
+
points = [
|
|
17
|
+
(round(24 - utils.lon_to_ra(lon), 4), round(dec, 4)) for lon, dec in points
|
|
18
|
+
]
|
|
19
|
+
return Polygon(points)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def to_24h(geometry: Union[Point, Polygon, MultiPolygon]):
|
|
23
|
+
def _to_poly24(p: Polygon):
|
|
24
|
+
coords = list(zip(*p.exterior.coords.xy))
|
|
25
|
+
coords = [(round(lon / 15, 4), round(dec, 4)) for lon, dec in coords]
|
|
26
|
+
return Polygon(coords)
|
|
27
|
+
|
|
28
|
+
def _to_point24(p: Point):
|
|
29
|
+
return Point(round(p.x / 15, 4), round(p.y, 4))
|
|
30
|
+
|
|
31
|
+
geometry_type = str(geometry.geom_type)
|
|
32
|
+
|
|
33
|
+
if geometry_type == "MultiPolygon":
|
|
34
|
+
polygons = []
|
|
35
|
+
for p in geometry.geoms:
|
|
36
|
+
p24 = _to_poly24(p)
|
|
37
|
+
polygons.append(p24)
|
|
38
|
+
return MultiPolygon(polygons)
|
|
39
|
+
elif geometry_type == "Polygon":
|
|
40
|
+
return _to_poly24(geometry)
|
|
41
|
+
elif geometry_type == "Point":
|
|
42
|
+
return _to_point24(geometry)
|
|
43
|
+
else:
|
|
44
|
+
raise ValueError(f"Unsupported geometry type: {geometry_type}")
|
starplot/models/moon.py
CHANGED
|
@@ -4,9 +4,11 @@ from enum import Enum
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
from skyfield.api import Angle, wgs84
|
|
6
6
|
from skyfield import almanac
|
|
7
|
+
from shapely import Polygon
|
|
7
8
|
|
|
8
9
|
from starplot.data import load
|
|
9
10
|
from starplot.models.base import SkyObject, SkyObjectManager
|
|
11
|
+
from starplot.models.geometry import circle
|
|
10
12
|
from starplot.utils import dt_or_now
|
|
11
13
|
|
|
12
14
|
|
|
@@ -112,6 +114,7 @@ class MoonManager(SkyObjectManager):
|
|
|
112
114
|
phase_angle=phase_angle,
|
|
113
115
|
phase_description=phase.value,
|
|
114
116
|
illumination=illumination,
|
|
117
|
+
geometry=circle((ra.hours, dec.degrees), apparent_diameter_degrees),
|
|
115
118
|
)
|
|
116
119
|
|
|
117
120
|
|
|
@@ -138,6 +141,9 @@ class Moon(SkyObject):
|
|
|
138
141
|
illumination: float
|
|
139
142
|
"""Percent of illumination (0...1)"""
|
|
140
143
|
|
|
144
|
+
geometry: Polygon = None
|
|
145
|
+
"""Shapely Polygon of the moon's extent. Right ascension coordinates are in 24H format."""
|
|
146
|
+
|
|
141
147
|
def __init__(
|
|
142
148
|
self,
|
|
143
149
|
ra: float,
|
|
@@ -148,6 +154,7 @@ class Moon(SkyObject):
|
|
|
148
154
|
phase_angle: float,
|
|
149
155
|
phase_description: str,
|
|
150
156
|
illumination: str,
|
|
157
|
+
geometry: Polygon = None,
|
|
151
158
|
) -> None:
|
|
152
159
|
super().__init__(ra, dec)
|
|
153
160
|
self.name = name
|
|
@@ -156,6 +163,7 @@ class Moon(SkyObject):
|
|
|
156
163
|
self.phase_angle = phase_angle
|
|
157
164
|
self.phase_description = phase_description
|
|
158
165
|
self.illumination = illumination
|
|
166
|
+
self.geometry = geometry
|
|
159
167
|
|
|
160
168
|
@classmethod
|
|
161
169
|
def get(
|
starplot/models/objects.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from starplot.models import Star, DSO, Moon, Sun, Planet
|
|
1
|
+
from starplot.models import Star, DSO, Moon, Sun, Planet, Constellation
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class ObjectList(object):
|
|
@@ -7,6 +7,9 @@ class ObjectList(object):
|
|
|
7
7
|
stars: list[Star] = None
|
|
8
8
|
"""Stars"""
|
|
9
9
|
|
|
10
|
+
constellations: list[Constellation] = None
|
|
11
|
+
"""Constellations"""
|
|
12
|
+
|
|
10
13
|
dsos: list[DSO] = None
|
|
11
14
|
"""Deep Sky Objects (DSOs)"""
|
|
12
15
|
|
|
@@ -23,3 +26,4 @@ class ObjectList(object):
|
|
|
23
26
|
self.stars = []
|
|
24
27
|
self.dsos = []
|
|
25
28
|
self.planets = []
|
|
29
|
+
self.constellations = []
|
starplot/models/planet.py
CHANGED
|
@@ -5,9 +5,11 @@ from typing import Iterator
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
|
|
7
7
|
from skyfield.api import Angle, wgs84
|
|
8
|
+
from shapely import Polygon
|
|
8
9
|
|
|
9
10
|
from starplot.data import load
|
|
10
11
|
from starplot.models.base import SkyObject, SkyObjectManager
|
|
12
|
+
from starplot.models.geometry import circle
|
|
11
13
|
from starplot.utils import dt_or_now
|
|
12
14
|
|
|
13
15
|
|
|
@@ -84,6 +86,7 @@ class PlanetManager(SkyObjectManager):
|
|
|
84
86
|
name=p,
|
|
85
87
|
dt=dt,
|
|
86
88
|
apparent_size=apparent_diameter_degrees,
|
|
89
|
+
geometry=circle((ra.hours, dec.degrees), apparent_diameter_degrees),
|
|
87
90
|
)
|
|
88
91
|
|
|
89
92
|
@classmethod
|
|
@@ -133,13 +136,23 @@ class Planet(SkyObject):
|
|
|
133
136
|
apparent_size: float
|
|
134
137
|
"""Apparent size (degrees)"""
|
|
135
138
|
|
|
139
|
+
geometry: Polygon = None
|
|
140
|
+
"""Shapely Polygon of the planet's extent. Right ascension coordinates are in 24H format."""
|
|
141
|
+
|
|
136
142
|
def __init__(
|
|
137
|
-
self,
|
|
143
|
+
self,
|
|
144
|
+
ra: float,
|
|
145
|
+
dec: float,
|
|
146
|
+
name: str,
|
|
147
|
+
dt: datetime,
|
|
148
|
+
apparent_size: float,
|
|
149
|
+
geometry: Polygon = None,
|
|
138
150
|
) -> None:
|
|
139
151
|
super().__init__(ra, dec)
|
|
140
152
|
self.name = name
|
|
141
153
|
self.dt = dt
|
|
142
154
|
self.apparent_size = apparent_size
|
|
155
|
+
self.geometry = geometry
|
|
143
156
|
|
|
144
157
|
@classmethod
|
|
145
158
|
def all(
|