starplot 0.12.5__py2.py3-none-any.whl → 0.14.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.

Files changed (73) hide show
  1. starplot/__init__.py +3 -2
  2. starplot/base.py +408 -95
  3. starplot/callables.py +61 -7
  4. starplot/coordinates.py +6 -0
  5. starplot/data/bayer.py +1532 -3
  6. starplot/data/constellations.py +564 -2
  7. starplot/data/flamsteed.py +2682 -0
  8. starplot/data/library/constellation_borders_inv.gpkg +0 -0
  9. starplot/data/library/constellation_lines_hips.json +3 -1
  10. starplot/data/stars.py +408 -87
  11. starplot/geometry.py +82 -0
  12. starplot/horizon.py +458 -0
  13. starplot/map.py +97 -284
  14. starplot/models/base.py +9 -2
  15. starplot/models/constellation.py +1 -1
  16. starplot/optic.py +32 -14
  17. starplot/plotters/__init__.py +2 -0
  18. starplot/plotters/constellations.py +339 -0
  19. starplot/plotters/dsos.py +5 -1
  20. starplot/plotters/experimental.py +171 -0
  21. starplot/plotters/milkyway.py +41 -0
  22. starplot/plotters/stars.py +143 -13
  23. starplot/styles/base.py +308 -169
  24. starplot/styles/ext/antique.yml +54 -46
  25. starplot/styles/ext/blue_dark.yml +39 -45
  26. starplot/styles/ext/blue_light.yml +49 -30
  27. starplot/styles/ext/blue_medium.yml +53 -50
  28. starplot/styles/ext/cb_wong.yml +16 -7
  29. starplot/styles/ext/grayscale.yml +17 -10
  30. starplot/styles/ext/grayscale_dark.yml +18 -8
  31. starplot/styles/ext/map.yml +10 -7
  32. starplot/styles/ext/nord.yml +38 -38
  33. starplot/styles/ext/optic.yml +7 -5
  34. starplot/styles/fonts-library/gfs-didot/DESCRIPTION.en_us.html +9 -0
  35. starplot/styles/fonts-library/gfs-didot/GFSDidot-Regular.ttf +0 -0
  36. starplot/styles/fonts-library/gfs-didot/METADATA.pb +16 -0
  37. starplot/styles/fonts-library/gfs-didot/OFL.txt +94 -0
  38. starplot/styles/fonts-library/hind/DESCRIPTION.en_us.html +28 -0
  39. starplot/styles/fonts-library/hind/Hind-Bold.ttf +0 -0
  40. starplot/styles/fonts-library/hind/Hind-Light.ttf +0 -0
  41. starplot/styles/fonts-library/hind/Hind-Medium.ttf +0 -0
  42. starplot/styles/fonts-library/hind/Hind-Regular.ttf +0 -0
  43. starplot/styles/fonts-library/hind/Hind-SemiBold.ttf +0 -0
  44. starplot/styles/fonts-library/hind/METADATA.pb +58 -0
  45. starplot/styles/fonts-library/hind/OFL.txt +93 -0
  46. starplot/styles/fonts-library/inter/Inter-Black.ttf +0 -0
  47. starplot/styles/fonts-library/inter/Inter-BlackItalic.ttf +0 -0
  48. starplot/styles/fonts-library/inter/Inter-Bold.ttf +0 -0
  49. starplot/styles/fonts-library/inter/Inter-BoldItalic.ttf +0 -0
  50. starplot/styles/fonts-library/inter/Inter-ExtraBold.ttf +0 -0
  51. starplot/styles/fonts-library/inter/Inter-ExtraBoldItalic.ttf +0 -0
  52. starplot/styles/fonts-library/inter/Inter-ExtraLight.ttf +0 -0
  53. starplot/styles/fonts-library/inter/Inter-ExtraLightItalic.ttf +0 -0
  54. starplot/styles/fonts-library/inter/Inter-Italic.ttf +0 -0
  55. starplot/styles/fonts-library/inter/Inter-Light.ttf +0 -0
  56. starplot/styles/fonts-library/inter/Inter-LightItalic.ttf +0 -0
  57. starplot/styles/fonts-library/inter/Inter-Medium.ttf +0 -0
  58. starplot/styles/fonts-library/inter/Inter-MediumItalic.ttf +0 -0
  59. starplot/styles/fonts-library/inter/Inter-Regular.ttf +0 -0
  60. starplot/styles/fonts-library/inter/Inter-SemiBold.ttf +0 -0
  61. starplot/styles/fonts-library/inter/Inter-SemiBoldItalic.ttf +0 -0
  62. starplot/styles/fonts-library/inter/Inter-Thin.ttf +0 -0
  63. starplot/styles/fonts-library/inter/Inter-ThinItalic.ttf +0 -0
  64. starplot/styles/fonts-library/inter/LICENSE.txt +92 -0
  65. starplot/styles/fonts.py +15 -0
  66. starplot/styles/markers.py +207 -6
  67. starplot/utils.py +19 -0
  68. starplot/warnings.py +16 -0
  69. {starplot-0.12.5.dist-info → starplot-0.14.0.dist-info}/METADATA +12 -12
  70. starplot-0.14.0.dist-info/RECORD +107 -0
  71. starplot-0.12.5.dist-info/RECORD +0 -67
  72. {starplot-0.12.5.dist-info → starplot-0.14.0.dist-info}/LICENSE +0 -0
  73. {starplot-0.12.5.dist-info → starplot-0.14.0.dist-info}/WHEEL +0 -0
starplot/optic.py CHANGED
@@ -7,8 +7,9 @@ from cartopy import crs as ccrs
7
7
  from matplotlib import pyplot as plt, patches, path
8
8
  from skyfield.api import wgs84, Star as SkyfieldStar
9
9
 
10
+ from starplot.coordinates import CoordinateSystem
10
11
  from starplot import callables
11
- from starplot.base import BasePlot
12
+ from starplot.base import BasePlot, DPI
12
13
  from starplot.data.stars import StarCatalog, STAR_NAMES
13
14
  from starplot.mixins import ExtentMaskMixin
14
15
  from starplot.models import Star
@@ -44,12 +45,17 @@ class OpticPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
44
45
  resolution: Size (in pixels) of largest dimension of the map
45
46
  hide_colliding_labels: If True, then labels will not be plotted if they collide with another existing label
46
47
  raise_on_below_horizon: If True, then a ValueError will be raised if the target is below the horizon at the observing time/location
48
+ scale: Scaling factor that will be applied to all sizes in styles (e.g. font size, marker size, line widths, etc). For example, if you want to make everything 2x bigger, then set the scale to 2. At `scale=1` and `resolution=4096` (the default), all sizes are optimized visually for a map that covers 1-3 constellations. So, if you're creating a plot of a _larger_ extent, then it'd probably be good to decrease the scale (i.e. make everything smaller) -- and _increase_ the scale if you're plotting a very small area.
49
+ autoscale: If True, then the scale will be set automatically based on resolution.
50
+ suppress_warnings: If True (the default), then all warnings will be suppressed
47
51
 
48
52
  Returns:
49
53
  OpticPlot: A new instance of an OpticPlot
50
54
 
51
55
  """
52
56
 
57
+ _coordinate_system = CoordinateSystem.AZ_ALT
58
+
53
59
  FIELD_OF_VIEW_MAX = 9.0
54
60
 
55
61
  def __init__(
@@ -62,9 +68,12 @@ class OpticPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
62
68
  dt: datetime = None,
63
69
  ephemeris: str = "de421_2001.bsp",
64
70
  style: PlotStyle = DEFAULT_OPTIC_STYLE,
65
- resolution: int = 2048,
71
+ resolution: int = 4096,
66
72
  hide_colliding_labels: bool = True,
67
73
  raise_on_below_horizon: bool = True,
74
+ scale: float = 1.0,
75
+ autoscale: bool = False,
76
+ suppress_warnings: bool = True,
68
77
  *args,
69
78
  **kwargs,
70
79
  ) -> "OpticPlot":
@@ -74,6 +83,9 @@ class OpticPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
74
83
  style,
75
84
  resolution,
76
85
  hide_colliding_labels,
86
+ scale=scale,
87
+ autoscale=autoscale,
88
+ suppress_warnings=suppress_warnings,
77
89
  *args,
78
90
  **kwargs,
79
91
  )
@@ -193,6 +205,7 @@ class OpticPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
193
205
  else:
194
206
  plotted.set_clip_path(self._background_clip_path)
195
207
 
208
+ @use_style(ObjectStyle, "star")
196
209
  def stars(
197
210
  self,
198
211
  mag: float = 6.0,
@@ -207,6 +220,7 @@ class OpticPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
207
220
  labels: Mapping[int, str] = STAR_NAMES,
208
221
  legend_label: str = "Star",
209
222
  bayer_labels: bool = False,
223
+ flamsteed_labels: bool = False,
210
224
  *args,
211
225
  **kwargs,
212
226
  ):
@@ -225,12 +239,16 @@ class OpticPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
225
239
  where_labels: A list of expressions that determine which stars are labeled on the plot. See [Selecting Objects](/reference-selecting-objects/) for details.
226
240
  labels: A dictionary that maps a star's HIP id to the label that'll be plotted for that star. If you want to hide name labels, then set this arg to `None`.
227
241
  legend_label: Label for stars in the legend. If `None`, then they will not be in the legend.
228
- bayer_labels: If True, then Bayer labels for stars will be plotted. Set this to False if you want to hide Bayer labels.
242
+ bayer_labels: If True, then Bayer labels for stars will be plotted.
243
+ flamsteed_labels: If True, then Flamsteed number labels for stars will be plotted.
229
244
  """
230
- optic_star_multiplier = 0.4 * (self.FIELD_OF_VIEW_MAX / self.optic.true_fov)
245
+ optic_star_multiplier = self.FIELD_OF_VIEW_MAX / self.optic.true_fov
246
+ size_fn_mx = None
247
+
248
+ if size_fn is not None:
231
249
 
232
- def size_fn_mx(st: Star) -> float:
233
- return size_fn(st) * optic_star_multiplier
250
+ def size_fn_mx(s):
251
+ return size_fn(s) * optic_star_multiplier * 0.68
234
252
 
235
253
  super().stars(
236
254
  mag=mag,
@@ -245,6 +263,7 @@ class OpticPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
245
263
  labels=labels,
246
264
  legend_label=legend_label,
247
265
  bayer_labels=bayer_labels,
266
+ flamsteed_labels=flamsteed_labels,
248
267
  *args,
249
268
  **kwargs,
250
269
  )
@@ -268,7 +287,7 @@ class OpticPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
268
287
  ) # apply transform again because new xy limits will undo the transform
269
288
 
270
289
  dt_str = self.dt.strftime("%m/%d/%Y @ %H:%M:%S") + " " + self.dt.tzname()
271
- font_size = style.font_size * self._size_multiplier * 2
290
+ font_size = style.font_size * self.scale
272
291
 
273
292
  column_labels = [
274
293
  "Target (Alt/Az)",
@@ -296,19 +315,17 @@ class OpticPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
296
315
  edges="vertical",
297
316
  )
298
317
  table.auto_set_font_size(False)
299
- table.set_fontsize(font_size)
318
+ table.set_fontsize(style.font_size)
300
319
  table.scale(1, 3.1)
301
320
 
302
321
  # Apply style to all cells
303
322
  for row in [0, 1]:
304
323
  for col in range(len(values)):
305
- table[row, col].set_text_props(
306
- **style.matplot_kwargs(self._size_multiplier)
307
- )
324
+ table[row, col].set_text_props(**style.matplot_kwargs(self.scale))
308
325
 
309
326
  # Apply some styles only to the header row
310
327
  for col in range(len(values)):
311
- table[0, col].set_text_props(fontweight="heavy", fontsize=font_size * 1.15)
328
+ table[0, col].set_text_props(fontweight="heavy", fontsize=font_size * 1.2)
312
329
 
313
330
  def _plot_border(self):
314
331
  # since we're using AzimuthalEquidistant projection, the center will always be (0, 0)
@@ -330,7 +347,7 @@ class OpticPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
330
347
  inner_border = self.optic.patch(
331
348
  x,
332
349
  y,
333
- linewidth=2 * self._size_multiplier,
350
+ linewidth=2 * self.scale,
334
351
  edgecolor=self.style.border_line_color.as_hex(),
335
352
  fill=False,
336
353
  zorder=ZOrderEnum.LAYER_5 + 100,
@@ -342,7 +359,7 @@ class OpticPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
342
359
  x,
343
360
  y,
344
361
  padding=0.05,
345
- linewidth=20 * self._size_multiplier,
362
+ linewidth=20 * self.scale,
346
363
  edgecolor=self.style.border_bg_color.as_hex(),
347
364
  fill=False,
348
365
  zorder=ZOrderEnum.LAYER_5,
@@ -366,6 +383,7 @@ class OpticPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
366
383
  figsize=(self.figure_size, self.figure_size),
367
384
  facecolor=self.style.figure_background_color.as_hex(),
368
385
  layout="constrained",
386
+ dpi=DPI,
369
387
  )
370
388
  self.ax = plt.axes(projection=self._proj)
371
389
  self.ax.xaxis.set_visible(False)
@@ -1,2 +1,4 @@
1
+ from .constellations import ConstellationPlotterMixin # noqa: F401
1
2
  from .stars import StarPlotterMixin # noqa: F401
2
3
  from .dsos import DsoPlotterMixin # noqa: F401
4
+ from .milkyway import MilkyWayPlotterMixin # noqa: F401
@@ -0,0 +1,339 @@
1
+ import geopandas as gpd
2
+ import numpy as np
3
+
4
+ import rtree
5
+ from shapely import (
6
+ MultiPoint,
7
+ )
8
+ from matplotlib.collections import LineCollection
9
+
10
+ from starplot.coordinates import CoordinateSystem
11
+ from starplot.data import DataFiles, constellations as condata, stars
12
+ from starplot.data.constellations import (
13
+ CONSTELLATIONS_FULL_NAMES,
14
+ CONSTELLATION_HIP_IDS,
15
+ )
16
+ from starplot.models.constellation import from_tuple as constellation_from_tuple
17
+ from starplot.projections import Projection
18
+ from starplot.styles import PathStyle, LineStyle, LabelStyle
19
+ from starplot.styles.helpers import use_style
20
+ from starplot.utils import points_on_line
21
+ from starplot.geometry import wrapped_polygon_adjustment
22
+
23
+ DEFAULT_AUTO_ADJUST_SETTINGS = {
24
+ "avoid_constellation_lines": False,
25
+ "point_generation_max_iterations": 500,
26
+ "distance_step_size": 1,
27
+ "max_distance": 300,
28
+ "label_padding": 9,
29
+ "buffer": 0.05,
30
+ "seed": None,
31
+ }
32
+ """Default settings for auto-adjusting constellation labels"""
33
+
34
+
35
+ class ConstellationPlotterMixin:
36
+ @use_style(LineStyle, "constellation_lines")
37
+ def constellations(
38
+ self,
39
+ style: LineStyle = None,
40
+ where: list = None,
41
+ ):
42
+ """Plots the constellation lines **only**. To plot constellation borders and/or labels, see separate functions for them.
43
+
44
+ **Important:** If you're plotting the constellation lines, then it's good to plot them _first_, because Starplot will use the constellation lines to determine where to place labels that are plotted afterwards (labels will look better if they're not crossing a constellation line).
45
+
46
+ Args:
47
+ style: Styling of the constellations. If None, then the plot's style (specified when creating the plot) will be used
48
+ where: A list of expressions that determine which constellations to plot. See [Selecting Objects](/reference-selecting-objects/) for details.
49
+ """
50
+ self.logger.debug("Plotting constellation lines...")
51
+
52
+ where = where or []
53
+ ctr = 0
54
+
55
+ constellations_gdf = gpd.read_file(
56
+ DataFiles.CONSTELLATIONS.value,
57
+ engine="pyogrio",
58
+ use_arrow=True,
59
+ bbox=self._extent_mask(),
60
+ )
61
+ stars_df = stars.load("hipparcos")
62
+
63
+ if constellations_gdf.empty:
64
+ return
65
+
66
+ if getattr(self, "projection", None) in [
67
+ Projection.MERCATOR,
68
+ Projection.MILLER,
69
+ ]:
70
+ transform = self._plate_carree
71
+ else:
72
+ transform = self._geodetic
73
+
74
+ conline_hips = condata.lines()
75
+ style_kwargs = style.matplot_kwargs(self.scale)
76
+ constellation_points_to_index = []
77
+ lines = []
78
+
79
+ for c in constellations_gdf.itertuples():
80
+ obj = constellation_from_tuple(c)
81
+
82
+ if not all([e.evaluate(obj) for e in where]):
83
+ continue
84
+
85
+ hiplines = conline_hips[c.iau_id]
86
+ inbounds = False
87
+
88
+ for s1_hip, s2_hip in hiplines:
89
+ s1 = stars_df.loc[s1_hip]
90
+ s2 = stars_df.loc[s2_hip]
91
+
92
+ s1_ra = s1.ra_hours * 15
93
+ s2_ra = s2.ra_hours * 15
94
+
95
+ s1_dec = s1.dec_degrees
96
+ s2_dec = s2.dec_degrees
97
+
98
+ if s1_ra - s2_ra > 60:
99
+ s2_ra += 360
100
+
101
+ elif s2_ra - s1_ra > 60:
102
+ s1_ra += 360
103
+
104
+ if not inbounds and self.in_bounds(s1.ra_hours, s1_dec):
105
+ inbounds = True
106
+
107
+ if self._coordinate_system == CoordinateSystem.RA_DEC:
108
+ s1_ra *= -1
109
+ s2_ra *= -1
110
+ x1, x2 = s1_ra, s2_ra
111
+ y1, y2 = s1_dec, s2_dec
112
+ elif self._coordinate_system == CoordinateSystem.AZ_ALT:
113
+ x1, y1 = self._prepare_coords(s1_ra / 15, s1_dec)
114
+ x2, y2 = self._prepare_coords(s2_ra / 15, s2_dec)
115
+ else:
116
+ raise ValueError("Unrecognized coordinate system")
117
+
118
+ lines.append([(x1, y1), (x2, y2)])
119
+
120
+ start = self._proj.transform_point(x1, y1, self._geodetic)
121
+ end = self._proj.transform_point(x2, y2, self._geodetic)
122
+ radius = style.width or 1
123
+
124
+ if any([np.isnan(n) for n in start + end]):
125
+ continue
126
+
127
+ for x, y in points_on_line(start, end, 25):
128
+ display_x, display_y = self.ax.transData.transform((x, y))
129
+ if display_x < 0 or display_y < 0:
130
+ continue
131
+ constellation_points_to_index.append(
132
+ (
133
+ ctr,
134
+ (
135
+ display_x - radius,
136
+ display_y - radius,
137
+ display_x + radius,
138
+ display_y + radius,
139
+ ),
140
+ None,
141
+ )
142
+ )
143
+ ctr += 1
144
+
145
+ if inbounds:
146
+ self._objects.constellations.append(obj)
147
+
148
+ style_kwargs = style.matplot_line_collection_kwargs(self.scale)
149
+
150
+ line_collection = LineCollection(
151
+ lines,
152
+ **style_kwargs,
153
+ transform=transform,
154
+ clip_on=True,
155
+ clip_path=self._background_clip_path,
156
+ gid="constellations-line",
157
+ )
158
+
159
+ self.ax.add_collection(line_collection)
160
+
161
+ if self._constellations_rtree.get_size() == 0:
162
+ self._constellations_rtree = rtree.index.Index(
163
+ constellation_points_to_index
164
+ )
165
+ else:
166
+ for bbox in constellation_points_to_index:
167
+ self._constellations_rtree.insert(
168
+ 0,
169
+ bbox,
170
+ None,
171
+ )
172
+ # self._plot_constellation_labels(style.label, labels_to_plot)
173
+ # self._plot_constellation_labels_experimental(style.label, labels_to_plot)
174
+
175
+ def _plot_constellation_labels(
176
+ self,
177
+ style: PathStyle = None,
178
+ labels: dict[str, str] = CONSTELLATIONS_FULL_NAMES,
179
+ ):
180
+ """
181
+ TODO:
182
+ 1. plot label, if removed then get size in display coords
183
+ 2. generate random points in polygon, convert to display coords, test for intersections
184
+ 3. plot best score
185
+
186
+ problem = constellations usually plotted first, so wont have star data (or could use stars from constellations only?)
187
+
188
+ constellation names CAN cross lines but not stars
189
+
190
+ """
191
+ style = style or self.style.constellation.label
192
+ self._constellation_labels = []
193
+
194
+ for con in condata.iterator():
195
+ _, ra, dec = condata.get(con)
196
+ text = labels.get(con.lower())
197
+ label = self.text(
198
+ text,
199
+ ra,
200
+ dec,
201
+ style,
202
+ hide_on_collision=False,
203
+ # hide_on_collision=self.hide_colliding_labels,
204
+ gid="constellations-label-name",
205
+ )
206
+ if label is not None:
207
+ self._constellation_labels.append(label)
208
+
209
+ @use_style(LineStyle, "constellation_borders")
210
+ def constellation_borders(self, style: LineStyle = None):
211
+ """Plots the constellation borders
212
+
213
+ Args:
214
+ style: Styling of the constellation borders. If None, then the plot's style (specified when creating the plot) will be used
215
+ """
216
+ constellation_borders = self._read_geo_package(
217
+ DataFiles.CONSTELLATION_BORDERS.value
218
+ )
219
+
220
+ if constellation_borders.empty:
221
+ return
222
+
223
+ geometries = []
224
+ border_lines = []
225
+ transform = self._plate_carree
226
+
227
+ for _, c in constellation_borders.iterrows():
228
+ for ls in c.geometry.geoms:
229
+ geometries.append(ls)
230
+
231
+ for ls in geometries:
232
+ x, y = ls.xy
233
+ x = list(x)
234
+ y = list(y)
235
+
236
+ if self._coordinate_system == CoordinateSystem.RA_DEC:
237
+ border_lines.append(list(zip(x, y)))
238
+
239
+ elif self._coordinate_system == CoordinateSystem.AZ_ALT:
240
+ x = [24 - (x0 / 15) for x0 in x]
241
+ coords = [self._prepare_coords(*p) for p in list(zip(x, y))]
242
+ border_lines.append(coords)
243
+ transform = self._crs
244
+
245
+ else:
246
+ raise ValueError("Unrecognized coordinate system")
247
+
248
+ style_kwargs = style.matplot_line_collection_kwargs(self.scale)
249
+
250
+ line_collection = LineCollection(
251
+ border_lines,
252
+ **style_kwargs,
253
+ transform=transform,
254
+ clip_on=True,
255
+ clip_path=self._background_clip_path,
256
+ gid="constellations-border",
257
+ )
258
+
259
+ self.ax.add_collection(line_collection)
260
+
261
+ def _constellation_labels_auto(self, style, labels, settings):
262
+ for constellation in self.objects.constellations:
263
+ constellation_line_stars = [
264
+ s
265
+ for s in self.objects.stars
266
+ if s.hip in CONSTELLATION_HIP_IDS[constellation.iau_id]
267
+ ]
268
+ if not constellation_line_stars:
269
+ continue
270
+
271
+ points_line = MultiPoint([(s.ra, s.dec) for s in constellation_line_stars])
272
+ centroid = points_line.centroid
273
+
274
+ adjustment = wrapped_polygon_adjustment(constellation.boundary)
275
+
276
+ if (adjustment > 0 and centroid.x < 12) or (
277
+ adjustment < 0 and centroid.x > 12
278
+ ):
279
+ x = centroid.x + adjustment
280
+ else:
281
+ x = centroid.x
282
+
283
+ text = labels.get(constellation.iau_id)
284
+
285
+ self.text(
286
+ text,
287
+ x,
288
+ centroid.y,
289
+ style,
290
+ hide_on_collision=self.hide_colliding_labels,
291
+ area=constellation.boundary, # TODO : make this intersection with clip path
292
+ auto_adjust_settings=settings,
293
+ gid="constellations-label-name",
294
+ )
295
+
296
+ def _constellation_labels_static(self, style, labels):
297
+ for con in condata.iterator():
298
+ _, ra, dec = condata.get(con)
299
+ text = labels.get(con.lower())
300
+ self.text(
301
+ text,
302
+ ra,
303
+ dec,
304
+ style,
305
+ hide_on_collision=self.hide_colliding_labels,
306
+ gid="constellations-label-name",
307
+ )
308
+
309
+ @use_style(LabelStyle, "constellation_labels")
310
+ def constellation_labels(
311
+ self,
312
+ style: LabelStyle = None,
313
+ labels: dict[str, str] = CONSTELLATIONS_FULL_NAMES,
314
+ auto_adjust: bool = True,
315
+ auto_adjust_settings: dict = DEFAULT_AUTO_ADJUST_SETTINGS,
316
+ ):
317
+ """
318
+ Plots constellation labels.
319
+
320
+ It's good to plot these last because they're area-based labels (vs point-based, like for star names), and area-based labels have more freedom to move around. If you plot area-based labels first, then it would limit the available space for point-based labels.
321
+
322
+ Args:
323
+ style: Styling of the constellation labels. If None, then the plot's style (specified when creating the plot) will be used
324
+ labels: A dictionary where the keys are each constellation's 3-letter IAU abbreviation, and the values are how the constellation will be labeled on the plot.
325
+ auto_adjust: If True (the default), then labels will be automatically adjusted to avoid collisions with other labels and stars **Important: you must plot stars and constellations first for this to work**. This uses a fairly simple method: for each constellation it finds the centroid of all plotted constellation stars with lines and then generates random points in the constellation boundary starting at the centroid and then progressively increasing the distance from the centroid.
326
+ auto_adjust_settings: Optional settings for the auto adjustment algorithm.
327
+
328
+ TODO:
329
+ make this work without plotting constellations first
330
+
331
+ """
332
+ self.logger.debug("Plotting constellation labels...")
333
+
334
+ if auto_adjust:
335
+ settings = DEFAULT_AUTO_ADJUST_SETTINGS
336
+ settings.update(auto_adjust_settings)
337
+ self._constellation_labels_auto(style, labels, settings=settings)
338
+ else:
339
+ self._constellation_labels_static(style, labels)
starplot/plotters/dsos.py CHANGED
@@ -222,7 +222,9 @@ class DsoPlotterMixin:
222
222
  )
223
223
 
224
224
  if label:
225
- self.text(label, ra / 15, dec, style.label)
225
+ self.text(
226
+ label, ra / 15, dec, style.label, gid=f"dso-{d.type}-label"
227
+ )
226
228
 
227
229
  else:
228
230
  # if no major axis, then just plot as a marker
@@ -232,6 +234,8 @@ class DsoPlotterMixin:
232
234
  style=style,
233
235
  label=label,
234
236
  skip_bounds_check=True,
237
+ gid_marker=f"dso-{d.type}-marker",
238
+ gid_label=f"dso-{d.type}-label",
235
239
  )
236
240
 
237
241
  self._objects.dsos.append(_dso)
@@ -0,0 +1,171 @@
1
+ import geopandas as gpd
2
+
3
+ from shapely import MultiPolygon
4
+ from shapely import (
5
+ MultiPoint,
6
+ intersection,
7
+ delaunay_triangles,
8
+ distance,
9
+ )
10
+
11
+ from starplot.data import DataFiles
12
+ from starplot.data.constellations import (
13
+ CONSTELLATIONS_FULL_NAMES,
14
+ CONSTELLATION_HIP_IDS,
15
+ )
16
+ from starplot.styles import PathStyle
17
+
18
+
19
+ class ExperimentalPlotterMixin:
20
+ def _constellation_borders(self):
21
+ from shapely import LineString, MultiLineString
22
+ from shapely.ops import unary_union
23
+
24
+ constellation_borders = gpd.read_file(
25
+ DataFiles.CONSTELLATIONS.value,
26
+ engine="pyogrio",
27
+ use_arrow=True,
28
+ bbox=self._extent_mask(),
29
+ )
30
+
31
+ if constellation_borders.empty:
32
+ return
33
+
34
+ geometries = []
35
+
36
+ for i, constellation in constellation_borders.iterrows():
37
+ geometry_types = constellation.geometry.geom_type
38
+
39
+ # equinox = LineString([[0, 90], [0, -90]])
40
+ """
41
+ Problems:
42
+ - Need to handle multipolygon borders too (SER)
43
+ - Shapely's union doesn't handle geodesy (e.g. TRI + AND)
44
+ - ^^ TRI is plotted with ra < 360, but AND has ra > 360
45
+ - ^^ idea: create union first and then remove duplicate lines?
46
+
47
+ TODO: create new static data file of constellation border lines
48
+ """
49
+
50
+ if "Polygon" in geometry_types and "MultiPolygon" not in geometry_types:
51
+ polygons = [constellation.geometry]
52
+
53
+ elif "MultiPolygon" in geometry_types:
54
+ polygons = constellation.geometry.geoms
55
+
56
+ for p in polygons:
57
+ coords = list(zip(*p.exterior.coords.xy))
58
+ # coords = [(ra * -1, dec) for ra, dec in coords]
59
+
60
+ new_coords = []
61
+
62
+ for i, c in enumerate(coords):
63
+ ra, dec = c
64
+ if i > 0:
65
+ if new_coords[i - 1][0] - ra > 60:
66
+ ra += 360
67
+
68
+ elif ra - new_coords[i - 1][0] > 60:
69
+ new_coords[i - 1][0] += 360
70
+
71
+ new_coords.append([ra, dec])
72
+
73
+ ls = LineString(new_coords)
74
+ geometries.append(ls)
75
+
76
+ mls = MultiLineString(geometries)
77
+ geometries = unary_union(mls)
78
+
79
+ for ls in list(geometries.geoms):
80
+ x, y = ls.xy
81
+
82
+ self.line(zip(x, y), self.style.constellation_borders)
83
+ # x, y = ls.xy
84
+ # newx = [xx * -1 for xx in list(x)]
85
+ # self.ax.plot(
86
+ # # list(x),
87
+ # newx,
88
+ # list(y),
89
+ # # **self._plot_kwargs(),
90
+ # # transform=self._geodetic,
91
+ # transform=self._plate_carree,
92
+ # **style_kwargs,
93
+ # )
94
+
95
+ def _plot_constellation_labels_experimental(
96
+ self,
97
+ style: PathStyle = None,
98
+ labels: dict[str, str] = CONSTELLATIONS_FULL_NAMES,
99
+ ):
100
+ def sorter(g):
101
+ # higher score is better
102
+ d = distance(g.centroid, points_line.centroid)
103
+ # d = distance(g.centroid, constellation.boundary.centroid)
104
+ extent = abs(g.bounds[2] - g.bounds[0])
105
+ area = g.area / constellation.boundary.area
106
+ # return ((extent**3)) * area**2
107
+ # return ((extent**2) - (d/2)) * area**2
108
+ # print(str(extent) + " " + str(area) + " " + str(d))
109
+ return d**2 * -1
110
+ return (extent / 2 + area) - (d / 5)
111
+
112
+ for constellation in self.objects.constellations:
113
+ constellation_stars = [
114
+ s
115
+ for s in self.objects.stars
116
+ if s.constellation_id == constellation.iau_id and s.magnitude < 5
117
+ ]
118
+ constellation_line_stars = [
119
+ s
120
+ for s in self.objects.stars
121
+ if s.constellation_id == constellation.iau_id
122
+ and s.hip in CONSTELLATION_HIP_IDS[constellation.iau_id]
123
+ ]
124
+ points = MultiPoint([(s.ra, s.dec) for s in constellation_stars])
125
+ points_line = MultiPoint([(s.ra, s.dec) for s in constellation_line_stars])
126
+
127
+ triangles = delaunay_triangles(
128
+ geometry=points,
129
+ tolerance=2,
130
+ )
131
+ print(constellation.name + " " + str(len(triangles.geoms)))
132
+
133
+ polygons = []
134
+ for t in triangles.geoms:
135
+ try:
136
+ inter = intersection(t, constellation.boundary)
137
+ except Exception:
138
+ continue
139
+ if (
140
+ inter.geom_type == "Polygon"
141
+ and len(list(zip(*inter.exterior.coords.xy))) > 2
142
+ ):
143
+ polygons.append(inter)
144
+
145
+ p_by_area = {pg.area: pg for pg in polygons}
146
+ polygons_sorted = [
147
+ p_by_area[k] for k in sorted(p_by_area.keys(), reverse=True)
148
+ ]
149
+
150
+ # sort by combination of horizontal extent and area
151
+ polygons_sorted = sorted(polygons_sorted, key=sorter, reverse=True)
152
+
153
+ if len(polygons_sorted) > 0:
154
+ i = 0
155
+ ra, dec = polygons_sorted[i].centroid.x, polygons_sorted[i].centroid.y
156
+ else:
157
+ ra, dec = constellation.ra, constellation.dec
158
+
159
+ text = labels.get(constellation.iau_id)
160
+ style = style or self.style.constellation.label
161
+ style.anchor_point = "center"
162
+ self.text(
163
+ text,
164
+ ra,
165
+ dec,
166
+ style,
167
+ hide_on_collision=self.hide_colliding_labels,
168
+ area=MultiPolygon(polygons_sorted[:3])
169
+ if len(polygons_sorted)
170
+ else constellation.boundary,
171
+ )