starplot 0.11.4__py2.py3-none-any.whl → 0.12.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.
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 +77 -81
- 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.0.dist-info}/METADATA +4 -13
- starplot-0.12.0.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.0.dist-info}/LICENSE +0 -0
- {starplot-0.11.4.dist-info → starplot-0.12.0.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(
|
|
@@ -426,17 +452,17 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
426
452
|
mw = self._read_geo_package(DataFiles.MILKY_WAY.value)
|
|
427
453
|
|
|
428
454
|
if not mw.empty:
|
|
429
|
-
style_kwargs = style.matplot_kwargs(size_multiplier=self._size_multiplier)
|
|
430
|
-
style_kwargs.pop("fill", None)
|
|
431
|
-
|
|
432
455
|
# create union of all Milky Way patches
|
|
433
456
|
gs = mw.geometry.to_crs(self._plate_carree)
|
|
434
457
|
mw_union = gs.buffer(0.1).unary_union.buffer(-0.1)
|
|
458
|
+
points = list(zip(*mw_union.boundary.coords.xy))
|
|
435
459
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
460
|
+
# convert lon to RA and reverse so the coordinates are counterclockwise order
|
|
461
|
+
points = [(lon_to_ra(lon) * 15, dec) for lon, dec in reversed(points)]
|
|
462
|
+
|
|
463
|
+
self._polygon(
|
|
464
|
+
points,
|
|
465
|
+
style=style,
|
|
440
466
|
)
|
|
441
467
|
|
|
442
468
|
@use_style(ObjectStyle, "zenith")
|
|
@@ -616,8 +642,9 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
616
642
|
return dec_formatter_fn(x)
|
|
617
643
|
|
|
618
644
|
ra_locations = ra_locations or [x for x in range(24)]
|
|
619
|
-
dec_locations = dec_locations or [d for d in range(-
|
|
645
|
+
dec_locations = dec_locations or [d for d in range(-80, 90, 10)]
|
|
620
646
|
|
|
647
|
+
line_style_kwargs = style.line.matplot_kwargs()
|
|
621
648
|
gridlines = self.ax.gridlines(
|
|
622
649
|
draw_labels=labels,
|
|
623
650
|
x_inline=False,
|
|
@@ -625,23 +652,36 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
625
652
|
rotate_labels=False,
|
|
626
653
|
xpadding=12,
|
|
627
654
|
ypadding=12,
|
|
628
|
-
|
|
655
|
+
clip_on=True,
|
|
656
|
+
clip_path=self._background_clip_path,
|
|
657
|
+
**line_style_kwargs,
|
|
629
658
|
)
|
|
630
659
|
|
|
631
660
|
if labels:
|
|
632
661
|
self._axis_labels = True
|
|
633
662
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
663
|
+
label_style_kwargs = style.label.matplot_kwargs()
|
|
664
|
+
label_style_kwargs.pop("va")
|
|
665
|
+
label_style_kwargs.pop("ha")
|
|
666
|
+
|
|
667
|
+
if self.dec_max > 75 or self.dec_min < -75:
|
|
668
|
+
# if the extent is near the poles, then plot the RA gridlines again
|
|
669
|
+
# because cartopy does not extend lines to poles
|
|
670
|
+
for ra in ra_locations:
|
|
671
|
+
self.ax.plot(
|
|
672
|
+
(ra * 15, ra * 15),
|
|
673
|
+
(-90, 90),
|
|
674
|
+
**line_style_kwargs,
|
|
675
|
+
**self._plot_kwargs(),
|
|
676
|
+
)
|
|
637
677
|
|
|
638
678
|
gridlines.xlocator = FixedLocator([ra_to_lon(r) for r in ra_locations])
|
|
639
679
|
gridlines.xformatter = FuncFormatter(ra_formatter)
|
|
640
|
-
gridlines.xlabel_style =
|
|
680
|
+
gridlines.xlabel_style = label_style_kwargs
|
|
641
681
|
|
|
642
682
|
gridlines.ylocator = FixedLocator(dec_locations)
|
|
643
683
|
gridlines.yformatter = FuncFormatter(dec_formatter)
|
|
644
|
-
gridlines.ylabel_style =
|
|
684
|
+
gridlines.ylabel_style = label_style_kwargs
|
|
645
685
|
|
|
646
686
|
if tick_marks:
|
|
647
687
|
self._tick_marks(style, ra_tick_locations, dec_tick_locations)
|
|
@@ -754,7 +794,26 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
754
794
|
)
|
|
755
795
|
|
|
756
796
|
def _plot_background_clip_path(self):
|
|
757
|
-
|
|
797
|
+
def to_axes(points):
|
|
798
|
+
ax_points = []
|
|
799
|
+
|
|
800
|
+
for ra, dec in points:
|
|
801
|
+
x, y = self._proj.transform_point(ra * 15, dec, self._crs)
|
|
802
|
+
data_to_axes = self.ax.transData + self.ax.transAxes.inverted()
|
|
803
|
+
x_axes, y_axes = data_to_axes.transform((x, y))
|
|
804
|
+
ax_points.append([x_axes, y_axes])
|
|
805
|
+
return ax_points
|
|
806
|
+
|
|
807
|
+
if self.clip_path is not None:
|
|
808
|
+
points = list(zip(*self.clip_path.exterior.coords.xy))
|
|
809
|
+
self._background_clip_path = patches.Polygon(
|
|
810
|
+
to_axes(points),
|
|
811
|
+
facecolor=self.style.background_color.as_hex(),
|
|
812
|
+
fill=True,
|
|
813
|
+
zorder=-2_000,
|
|
814
|
+
transform=self.ax.transAxes,
|
|
815
|
+
)
|
|
816
|
+
elif self.projection == Projection.ZENITH:
|
|
758
817
|
self._background_clip_path = patches.Circle(
|
|
759
818
|
(0.50, 0.50),
|
|
760
819
|
radius=0.45,
|
|
@@ -780,66 +839,3 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
|
|
|
780
839
|
)
|
|
781
840
|
|
|
782
841
|
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(
|