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.

Files changed (42) hide show
  1. starplot/__init__.py +1 -1
  2. starplot/base.py +21 -1
  3. starplot/data/constellations.py +16 -1
  4. starplot/data/dsos.py +1 -1
  5. starplot/data/library/constellations.gpkg +0 -0
  6. starplot/data/library/ongc.gpkg.zip +0 -0
  7. starplot/data/prep/__init__.py +0 -0
  8. starplot/data/prep/constellations.py +108 -0
  9. starplot/data/prep/dsos.py +299 -0
  10. starplot/data/prep/utils.py +16 -0
  11. starplot/map.py +90 -84
  12. starplot/models/__init__.py +1 -1
  13. starplot/models/base.py +9 -3
  14. starplot/models/constellation.py +27 -5
  15. starplot/models/dso.py +47 -1
  16. starplot/models/geometry.py +44 -0
  17. starplot/models/moon.py +8 -0
  18. starplot/models/objects.py +5 -1
  19. starplot/models/planet.py +14 -1
  20. starplot/models/star.py +47 -7
  21. starplot/models/sun.py +14 -1
  22. starplot/optic.py +11 -4
  23. starplot/plotters/dsos.py +58 -28
  24. starplot/plotters/stars.py +15 -29
  25. starplot/styles/base.py +98 -52
  26. starplot/styles/ext/antique.yml +29 -1
  27. starplot/styles/ext/blue_dark.yml +20 -2
  28. starplot/styles/ext/blue_light.yml +29 -1
  29. starplot/styles/ext/blue_medium.yml +30 -1
  30. starplot/styles/ext/cb_wong.yml +28 -1
  31. starplot/styles/ext/color_print.yml +3 -0
  32. starplot/styles/ext/grayscale.yml +18 -1
  33. starplot/styles/ext/grayscale_dark.yml +20 -1
  34. starplot/styles/ext/nord.yml +33 -7
  35. starplot/styles/markers.py +107 -0
  36. starplot/utils.py +1 -1
  37. {starplot-0.11.4.dist-info → starplot-0.12.1.dist-info}/METADATA +4 -13
  38. starplot-0.12.1.dist-info/RECORD +67 -0
  39. starplot/data/library/de440s.bsp +0 -0
  40. starplot-0.11.4.dist-info/RECORD +0 -62
  41. {starplot-0.11.4.dist-info → starplot-0.12.1.dist-info}/LICENSE +0 -0
  42. {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 i, c in constellations_gdf.iterrows():
373
- hiplines = conline_hips[c.id]
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 not mw.empty:
429
- style_kwargs = style.matplot_kwargs(size_multiplier=self._size_multiplier)
430
- style_kwargs.pop("fill", None)
454
+ if mw.empty:
455
+ return
431
456
 
432
- # create union of all Milky Way patches
433
- gs = mw.geometry.to_crs(self._plate_carree)
434
- mw_union = gs.buffer(0.1).unary_union.buffer(-0.1)
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
- self.ax.add_geometries(
437
- [mw_union],
438
- crs=self._plate_carree,
439
- **style_kwargs,
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(-90, 90, 10)]
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
- **style.line.matplot_kwargs(),
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
- style_kwargs = style.label.matplot_kwargs()
635
- style_kwargs.pop("va")
636
- style_kwargs.pop("ha")
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 = style_kwargs
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 = style_kwargs
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
- if self.projection == Projection.ZENITH:
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)
@@ -4,4 +4,4 @@ from .star import Star # noqa: F401,F403
4
4
  from .planet import Planet # noqa: F401,F403
5
5
  from .moon import Moon # noqa: F401,F403
6
6
  from .sun import Sun # noqa: F401,F403
7
- from .objects import * # noqa: F401,F403
7
+ from .objects import ObjectList # noqa: F401,F403
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
- return [s for s in cls.all() if all([e.evaluate(s) for e in where])]
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 cls.all()
148
+ for s in all_objects
143
149
  if all([getattr(s, kw) == value for kw, value in kwargs.items()])
144
150
  ]
145
151
 
@@ -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
- 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
- )
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(
@@ -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, ra: float, dec: float, name: str, dt: datetime, apparent_size: float
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(