starplot 0.13.0__py2.py3-none-any.whl → 0.15.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.
Files changed (74) hide show
  1. starplot/__init__.py +5 -2
  2. starplot/base.py +311 -197
  3. starplot/cli.py +33 -0
  4. starplot/coordinates.py +6 -0
  5. starplot/data/__init__.py +6 -24
  6. starplot/data/bigsky.py +58 -40
  7. starplot/data/constellation_lines.py +827 -0
  8. starplot/data/constellation_stars.py +1501 -0
  9. starplot/data/constellations.py +600 -27
  10. starplot/data/db.py +17 -0
  11. starplot/data/dsos.py +24 -141
  12. starplot/data/stars.py +45 -24
  13. starplot/geod.py +0 -6
  14. starplot/geometry.py +181 -0
  15. starplot/horizon.py +302 -231
  16. starplot/map.py +100 -463
  17. starplot/mixins.py +75 -14
  18. starplot/models/__init__.py +1 -1
  19. starplot/models/base.py +18 -129
  20. starplot/models/constellation.py +55 -32
  21. starplot/models/dso.py +132 -67
  22. starplot/models/moon.py +57 -78
  23. starplot/models/planet.py +44 -69
  24. starplot/models/star.py +91 -60
  25. starplot/models/sun.py +32 -53
  26. starplot/optic.py +21 -18
  27. starplot/plotters/__init__.py +2 -0
  28. starplot/plotters/constellations.py +342 -0
  29. starplot/plotters/dsos.py +49 -68
  30. starplot/plotters/experimental.py +171 -0
  31. starplot/plotters/milkyway.py +39 -0
  32. starplot/plotters/stars.py +126 -122
  33. starplot/profile.py +16 -0
  34. starplot/settings.py +26 -0
  35. starplot/styles/__init__.py +2 -0
  36. starplot/styles/base.py +56 -34
  37. starplot/styles/ext/antique.yml +11 -9
  38. starplot/styles/ext/blue_dark.yml +8 -10
  39. starplot/styles/ext/blue_gold.yml +135 -0
  40. starplot/styles/ext/blue_light.yml +14 -12
  41. starplot/styles/ext/blue_medium.yml +23 -20
  42. starplot/styles/ext/cb_wong.yml +9 -7
  43. starplot/styles/ext/grayscale.yml +4 -3
  44. starplot/styles/ext/grayscale_dark.yml +7 -5
  45. starplot/styles/ext/map.yml +9 -6
  46. starplot/styles/ext/nord.yml +7 -7
  47. starplot/styles/ext/optic.yml +1 -1
  48. starplot/styles/extensions.py +1 -0
  49. starplot/utils.py +19 -0
  50. starplot/warnings.py +21 -0
  51. {starplot-0.13.0.dist-info → starplot-0.15.0.dist-info}/METADATA +19 -18
  52. starplot-0.15.0.dist-info/RECORD +97 -0
  53. starplot-0.15.0.dist-info/entry_points.txt +3 -0
  54. starplot/data/bayer.py +0 -3499
  55. starplot/data/flamsteed.py +0 -2682
  56. starplot/data/library/constellation_borders_inv.gpkg +0 -0
  57. starplot/data/library/constellation_lines_hips.json +0 -709
  58. starplot/data/library/constellation_lines_inv.gpkg +0 -0
  59. starplot/data/library/constellations.gpkg +0 -0
  60. starplot/data/library/constellations_hip.fab +0 -88
  61. starplot/data/library/milkyway.gpkg +0 -0
  62. starplot/data/library/milkyway_inv.gpkg +0 -0
  63. starplot/data/library/ongc.gpkg.zip +0 -0
  64. starplot/data/library/stars.bigsky.mag11.parquet +0 -0
  65. starplot/data/library/stars.hipparcos.parquet +0 -0
  66. starplot/data/messier.py +0 -111
  67. starplot/data/prep/__init__.py +0 -0
  68. starplot/data/prep/constellations.py +0 -108
  69. starplot/data/prep/dsos.py +0 -299
  70. starplot/data/prep/utils.py +0 -16
  71. starplot/models/geometry.py +0 -44
  72. starplot-0.13.0.dist-info/RECORD +0 -101
  73. {starplot-0.13.0.dist-info → starplot-0.15.0.dist-info}/LICENSE +0 -0
  74. {starplot-0.13.0.dist-info → starplot-0.15.0.dist-info}/WHEEL +0 -0
starplot/map.py CHANGED
@@ -1,63 +1,48 @@
1
1
  import datetime
2
2
  import math
3
- import warnings
4
3
  from typing import Callable
4
+ from functools import cache
5
5
 
6
6
  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, Polygon
11
- from shapely.ops import unary_union
12
- from skyfield.api import Star as SkyfieldStar, wgs84
13
- import geopandas as gpd
10
+ from shapely import Polygon
11
+ from skyfield.api import wgs84
14
12
  import numpy as np
15
13
 
14
+ from starplot.coordinates import CoordinateSystem
16
15
  from starplot import geod
17
16
  from starplot.base import BasePlot, DPI
18
- from starplot.data import DataFiles, constellations as condata, stars
19
- from starplot.data.constellations import CONSTELLATIONS_FULL_NAMES
20
17
  from starplot.mixins import ExtentMaskMixin
21
- from starplot.models.constellation import from_tuple as constellation_from_tuple
22
- from starplot.plotters import StarPlotterMixin, DsoPlotterMixin
18
+ from starplot.plotters import (
19
+ ConstellationPlotterMixin,
20
+ StarPlotterMixin,
21
+ DsoPlotterMixin,
22
+ MilkyWayPlotterMixin,
23
+ )
23
24
  from starplot.projections import Projection
24
25
  from starplot.styles import (
25
26
  ObjectStyle,
26
27
  LabelStyle,
27
- LineStyle,
28
28
  PlotStyle,
29
- PolygonStyle,
30
29
  PathStyle,
31
30
  )
32
31
  from starplot.styles.helpers import use_style
33
32
  from starplot.utils import lon_to_ra, ra_to_lon
34
33
 
35
- # Silence noisy cartopy warnings
36
- warnings.filterwarnings("ignore", module="cartopy")
37
- warnings.filterwarnings("ignore", module="shapely")
38
34
 
39
35
  DEFAULT_MAP_STYLE = PlotStyle() # .extend(extensions.MAP)
40
36
 
41
37
 
42
- def points(start, end, num_points=100):
43
- """Generates points along a line segment.
44
-
45
- Args:
46
- start (tuple): (x, y) coordinates of the starting point.
47
- end (tuple): (x, y) coordinates of the ending point.
48
- num_points (int): Number of points to generate.
49
-
50
- Returns:
51
- list: List of (x, y) coordinates of the generated points.
52
- """
53
-
54
- x_coords = np.linspace(start[0], end[0], num_points)
55
- y_coords = np.linspace(start[1], end[1], num_points)
56
-
57
- return list(zip(x_coords, y_coords))
58
-
59
-
60
- class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
38
+ class MapPlot(
39
+ BasePlot,
40
+ ExtentMaskMixin,
41
+ StarPlotterMixin,
42
+ DsoPlotterMixin,
43
+ MilkyWayPlotterMixin,
44
+ ConstellationPlotterMixin,
45
+ ):
61
46
  """Creates a new map plot.
62
47
 
63
48
  !!! star "Note"
@@ -65,8 +50,8 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
65
50
 
66
51
  Args:
67
52
  projection: Projection of the map
68
- ra_min: Minimum right ascension of the map's extent, in hours (0...24)
69
- ra_max: Maximum right ascension of the map's extent, in hours (0...24)
53
+ ra_min: Minimum right ascension of the map's extent, in degrees (0...360)
54
+ ra_max: Maximum right ascension of the map's extent, in degrees (0...360)
70
55
  dec_min: Minimum declination of the map's extent, in degrees (-90...90)
71
56
  dec_max: Maximum declination of the map's extent, in degrees (-90...90)
72
57
  lat: Latitude for perspective projections: Orthographic, Stereographic, and Zenith
@@ -79,17 +64,20 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
79
64
  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.
80
65
  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.
81
66
  autoscale: If True, then the scale will be set automatically based on resolution.
67
+ suppress_warnings: If True (the default), then all warnings will be suppressed
82
68
 
83
69
  Returns:
84
70
  MapPlot: A new instance of a MapPlot
85
71
 
86
72
  """
87
73
 
74
+ _coordinate_system = CoordinateSystem.RA_DEC
75
+
88
76
  def __init__(
89
77
  self,
90
78
  projection: Projection,
91
79
  ra_min: float = 0,
92
- ra_max: float = 24,
80
+ ra_max: float = 360,
93
81
  dec_min: float = -90,
94
82
  dec_max: float = 90,
95
83
  lat: float = None,
@@ -102,6 +90,7 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
102
90
  clip_path: Polygon = None,
103
91
  scale: float = 1.0,
104
92
  autoscale: bool = False,
93
+ suppress_warnings: bool = True,
105
94
  *args,
106
95
  **kwargs,
107
96
  ) -> "MapPlot":
@@ -113,6 +102,7 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
113
102
  hide_colliding_labels,
114
103
  scale=scale,
115
104
  autoscale=autoscale,
105
+ suppress_warnings=suppress_warnings,
116
106
  *args,
117
107
  **kwargs,
118
108
  )
@@ -145,7 +135,7 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
145
135
 
146
136
  if self.projection == Projection.ZENITH and not self._is_global_extent():
147
137
  raise ValueError(
148
- "Zenith projection requires a global extent: ra_min=0, ra_max=24, dec_min=-90, dec_max=90"
138
+ "Zenith projection requires a global extent: ra_min=0, ra_max=360, dec_min=-90, dec_max=90"
149
139
  )
150
140
 
151
141
  self._geodetic = ccrs.Geodetic()
@@ -163,27 +153,25 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
163
153
  def _plot_kwargs(self) -> dict:
164
154
  return dict(transform=self._crs)
165
155
 
166
- def _prepare_coords(self, ra: float, dec: float) -> (float, float):
167
- return ra * 15, dec
168
-
156
+ @cache
169
157
  def in_bounds(self, ra: float, dec: float) -> bool:
170
158
  """Determine if a coordinate is within the bounds of the plot.
171
159
 
172
160
  Args:
173
- ra: Right ascension, in hours (0...24)
161
+ ra: Right ascension, in degrees (0...360)
174
162
  dec: Declination, in degrees (-90...90)
175
163
 
176
164
  Returns:
177
165
  True if the coordinate is in bounds, otherwise False
178
166
  """
179
167
  # TODO : try using pyproj transformer directly
180
- x, y = self._proj.transform_point(ra * 15, dec, self._crs)
168
+ x, y = self._proj.transform_point(ra, dec, self._crs)
181
169
  data_to_axes = self.ax.transData + self.ax.transAxes.inverted()
182
170
  x_axes, y_axes = data_to_axes.transform((x, y))
183
171
  return 0 <= x_axes <= 1 and 0 <= y_axes <= 1
184
172
 
185
173
  def _in_bounds_xy(self, x: float, y: float) -> bool:
186
- return self.in_bounds(x / 15, y)
174
+ return self.in_bounds(x, y)
187
175
 
188
176
  def _polygon(self, points, style, **kwargs):
189
177
  super()._polygon(points, style, transform=self._crs, **kwargs)
@@ -191,409 +179,45 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
191
179
  def _latlon_bounds(self):
192
180
  # convert the RA/DEC bounds to lat/lon bounds
193
181
  return [
194
- -1 * self.ra_min * 15,
195
- -1 * self.ra_max * 15,
182
+ -1 * self.ra_min,
183
+ -1 * self.ra_max,
196
184
  self.dec_min,
197
185
  self.dec_max,
198
186
  ]
199
187
 
200
188
  def _adjust_radec_minmax(self):
189
+ # adjust declination to match extent
190
+ extent = self.ax.get_extent(crs=self._plate_carree)
191
+ self.dec_min = extent[2]
192
+ self.dec_max = extent[3]
193
+
201
194
  # adjust the RA min/max if the DEC bounds is near the poles
202
195
  if self.projection in [Projection.STEREO_NORTH, Projection.STEREO_SOUTH] and (
203
196
  self.dec_max > 80 or self.dec_min < -80
204
197
  ):
205
198
  self.ra_min = 0
206
- self.ra_max = 24
207
-
208
- # adjust declination to match extent
209
- extent = self.ax.get_extent(crs=self._plate_carree)
210
- self.dec_min = extent[2]
211
- self.dec_max = extent[3]
199
+ self.ra_max = 360
212
200
 
213
- # adjust right ascension to match extent
214
- if self.ra_max < 24:
215
- ra_min = (-1 * extent[1]) / 15
216
- ra_max = (-1 * extent[0]) / 15
201
+ elif self.ra_max < 360:
202
+ # adjust right ascension to match extent
203
+ ra_min = extent[1] * -1
204
+ ra_max = extent[0] * -1
217
205
 
218
206
  if ra_min < 0 or ra_max < 0:
219
- ra_min += 24
220
- ra_max += 24
207
+ ra_min += 360
208
+ ra_max += 360
221
209
 
222
210
  self.ra_min = ra_min
223
211
  self.ra_max = ra_max
224
212
 
225
- self.logger.debug(
226
- f"Extent = RA ({self.ra_min:.2f}, {self.ra_max:.2f}) DEC ({self.dec_min:.2f}, {self.dec_max:.2f})"
227
- )
228
-
229
- def _read_geo_package(self, filename: str):
230
- """Returns GeoDataFrame of a GeoPackage file"""
231
- extent = self.ax.get_extent(crs=self._plate_carree)
232
- bbox = (extent[0], extent[2], extent[1], extent[3])
233
-
234
- return gpd.read_file(
235
- filename,
236
- engine="pyogrio",
237
- use_arrow=True,
238
- bbox=bbox,
239
- )
240
-
241
- def _load_stars(self, catalog, limiting_magnitude):
242
- df = super()._load_stars(catalog, limiting_magnitude)
243
-
244
- if self.projection == Projection.ZENITH:
245
- # filter stars for zenith plots to only include those above horizon
246
- earth = self.ephemeris["earth"]
247
- self.location = earth + wgs84.latlon(self.lat, self.lon)
248
-
249
- stars_apparent = (
250
- self.location.at(self.timescale)
251
- .observe(SkyfieldStar.from_dataframe(df))
252
- .apparent()
253
- )
254
- # we only need altitude
255
- stars_alt, _, _ = stars_apparent.altaz()
256
- df["alt"] = stars_alt.degrees
257
- df = df[df["alt"] > 0]
258
-
259
- return df
260
-
261
- @use_style(LineStyle, "constellation_borders")
262
- def constellation_borders(self, style: LineStyle = None):
263
- """Plots the constellation borders
264
-
265
- Args:
266
- style: Styling of the constellation borders. If None, then the plot's style (specified when creating the plot) will be used
267
- """
268
- constellation_borders = self._read_geo_package(
269
- DataFiles.CONSTELLATION_BORDERS.value
270
- )
271
-
272
- if constellation_borders.empty:
273
- return
274
-
275
- style_kwargs = style.matplot_kwargs(self.scale)
276
-
277
- geometries = []
278
-
279
- for _, c in constellation_borders.iterrows():
280
- for ls in c.geometry.geoms:
281
- geometries.append(ls)
282
-
283
- for ls in geometries:
284
- x, y = ls.xy
285
- self.ax.plot(
286
- list(x),
287
- list(y),
288
- transform=self._plate_carree,
289
- clip_on=True,
290
- clip_path=self._background_clip_path,
291
- gid="constellations-border",
292
- **style_kwargs,
293
- )
294
-
295
- def _plot_constellation_borders(self):
296
- """work in progress"""
297
- constellation_borders = gpd.read_file(
298
- DataFiles.CONSTELLATIONS.value,
299
- engine="pyogrio",
300
- use_arrow=True,
301
- bbox=self._extent_mask(),
302
- )
303
-
304
- if constellation_borders.empty:
305
- return
306
-
307
- geometries = []
308
-
309
- for i, constellation in constellation_borders.iterrows():
310
- geometry_types = constellation.geometry.geom_type
311
-
312
- # equinox = LineString([[0, 90], [0, -90]])
313
- """
314
- Problems:
315
- - Need to handle multipolygon borders too (SER)
316
- - Shapely's union doesn't handle geodesy (e.g. TRI + AND)
317
- - ^^ TRI is plotted with ra < 360, but AND has ra > 360
318
- - ^^ idea: create union first and then remove duplicate lines?
319
-
320
- TODO: create new static data file of constellation border lines
321
- """
322
-
323
- if "Polygon" in geometry_types and "MultiPolygon" not in geometry_types:
324
- polygons = [constellation.geometry]
325
-
326
- elif "MultiPolygon" in geometry_types:
327
- polygons = constellation.geometry.geoms
328
-
329
- for p in polygons:
330
- coords = list(zip(*p.exterior.coords.xy))
331
- # coords = [(ra * -1, dec) for ra, dec in coords]
332
-
333
- new_coords = []
334
-
335
- for i, c in enumerate(coords):
336
- ra, dec = c
337
- if i > 0:
338
- if new_coords[i - 1][0] - ra > 60:
339
- ra += 360
340
-
341
- elif ra - new_coords[i - 1][0] > 60:
342
- new_coords[i - 1][0] += 360
343
-
344
- new_coords.append([ra, dec])
345
-
346
- ls = LineString(new_coords)
347
- geometries.append(ls)
348
-
349
- mls = MultiLineString(geometries)
350
- geometries = unary_union(mls)
351
-
352
- style_kwargs = self.style.constellation_borders.matplot_kwargs(self.scale)
353
-
354
- for ls in list(geometries.geoms):
355
- # print(ls)
356
- x, y = ls.xy
357
- newx = [xx * -1 for xx in list(x)]
358
- self.ax.plot(
359
- # list(x),
360
- newx,
361
- list(y),
362
- # **self._plot_kwargs(),
363
- # transform=self._geodetic,
364
- transform=self._plate_carree,
365
- **style_kwargs,
366
- )
367
-
368
- @use_style(PathStyle, "constellation")
369
- def constellations(
370
- self,
371
- style: PathStyle = None,
372
- labels: dict[str, str] = CONSTELLATIONS_FULL_NAMES,
373
- where: list = None,
374
- ):
375
- """Plots the constellation lines and/or labels.
376
-
377
- **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).
378
-
379
- Args:
380
- style: Styling of the constellations. If None, then the plot's style (specified when creating the plot) will be used
381
- 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.
382
- where: A list of expressions that determine which constellations to plot. See [Selecting Objects](/reference-selecting-objects/) for details.
383
- """
384
- self.logger.debug("Plotting constellations...")
385
-
386
- labels = labels or {}
387
- where = where or []
388
-
389
- constellations_gdf = gpd.read_file(
390
- DataFiles.CONSTELLATIONS.value,
391
- engine="pyogrio",
392
- use_arrow=True,
393
- bbox=self._extent_mask(),
394
- )
395
- stars_df = stars.load("hipparcos")
396
-
397
- if constellations_gdf.empty:
398
- return
399
-
400
- if self.projection in [Projection.MERCATOR, Projection.MILLER]:
401
- transform = self._plate_carree
402
213
  else:
403
- transform = self._geodetic
404
-
405
- conline_hips = condata.lines()
406
- style_kwargs = style.line.matplot_kwargs(self.scale)
407
-
408
- for c in constellations_gdf.itertuples():
409
- obj = constellation_from_tuple(c)
410
-
411
- if not all([e.evaluate(obj) for e in where]):
412
- continue
413
-
414
- hiplines = conline_hips[c.iau_id]
415
- inbounds = False
416
-
417
- for s1_hip, s2_hip in hiplines:
418
- s1 = stars_df.loc[s1_hip]
419
- s2 = stars_df.loc[s2_hip]
420
-
421
- s1_ra = s1.ra_hours * 15
422
- s2_ra = s2.ra_hours * 15
423
-
424
- s1_dec = s1.dec_degrees
425
- s2_dec = s2.dec_degrees
426
-
427
- if s1_ra - s2_ra > 60:
428
- s2_ra += 360
429
-
430
- elif s2_ra - s1_ra > 60:
431
- s1_ra += 360
432
-
433
- if self.in_bounds(s1_ra / 15, s1_dec):
434
- inbounds = True
435
-
436
- s1_ra *= -1
437
- s2_ra *= -1
438
-
439
- # make lines straight
440
- # s1_ra, s1_dec = self._proj.transform_point(s1_ra, s1.dec_degrees, self._geodetic)
441
- # s2_ra, s2_dec = self._proj.transform_point(s2_ra, s2.dec_degrees, self._geodetic)
442
-
443
- constellation_line = self.ax.plot(
444
- [s1_ra, s2_ra],
445
- [s1_dec, s2_dec],
446
- transform=transform,
447
- **style_kwargs,
448
- clip_on=True,
449
- clip_path=self._background_clip_path,
450
- gid="constellations-line",
451
- )[0]
452
-
453
- extent = constellation_line.get_window_extent(
454
- renderer=self.fig.canvas.get_renderer()
455
- )
456
-
457
- if extent.xmin < 0:
458
- continue
459
-
460
- start = self._proj.transform_point(s1_ra, s1_dec, self._geodetic)
461
- end = self._proj.transform_point(s2_ra, s2_dec, self._geodetic)
462
- radius = style_kwargs.get("linewidth") or 1
463
-
464
- if any([np.isnan(n) for n in start + end]):
465
- continue
466
-
467
- for x, y in points(start, end, 25):
468
- x0, y0 = self.ax.transData.transform((x, y))
469
- if x0 < 0 or y0 < 0:
470
- continue
471
- self._constellations_rtree.insert(
472
- 0,
473
- np.array((x0 - radius, y0 - radius, x0 + radius, y0 + radius)),
474
- obj=obj.name,
475
- )
476
-
477
- if inbounds:
478
- self._objects.constellations.append(obj)
479
-
480
- self._plot_constellation_labels(style.label, labels)
481
- # self._plot_constellation_labels_experimental(style.label, labels)
482
-
483
- def _plot_constellation_labels(
484
- self,
485
- style: PathStyle = None,
486
- labels: dict[str, str] = CONSTELLATIONS_FULL_NAMES,
487
- ):
488
- style = style or self.style.constellation.label
489
-
490
- for con in condata.iterator():
491
- _, ra, dec = condata.get(con)
492
- text = labels.get(con.lower())
493
- self.text(
494
- text,
495
- ra,
496
- dec,
497
- style,
498
- hide_on_collision=False,
499
- gid="constellations-label-name",
500
- )
214
+ self.ra_min = lon_to_ra(extent[1]) * 15
215
+ self.ra_max = lon_to_ra(extent[0]) * 15 + 360
501
216
 
502
- def _plot_constellation_labels_experimental(
503
- self,
504
- style: PathStyle = None,
505
- labels: dict[str, str] = CONSTELLATIONS_FULL_NAMES,
506
- ):
507
- from shapely import (
508
- MultiPoint,
509
- intersection,
510
- delaunay_triangles,
511
- distance,
217
+ self.logger.debug(
218
+ f"Extent = RA ({self.ra_min:.2f}, {self.ra_max:.2f}) DEC ({self.dec_min:.2f}, {self.dec_max:.2f})"
512
219
  )
513
220
 
514
- def sorter(g):
515
- d = distance(g.centroid, points.centroid)
516
- # d = distance(g.centroid, constellation.boundary.centroid)
517
- extent = abs(g.bounds[2] - g.bounds[0])
518
- area = g.area / constellation.boundary.area
519
- return (extent**2 + area) - (d**2)
520
-
521
- for constellation in self.objects.constellations:
522
- constellation_stars = [
523
- s
524
- for s in self.objects.stars
525
- if s.constellation_id == constellation.iau_id
526
- ]
527
- points = MultiPoint([(s.ra, s.dec) for s in constellation_stars])
528
-
529
- triangles = delaunay_triangles(
530
- geometry=points,
531
- # tolerance=2,
532
- )
533
-
534
- polygons = []
535
- for t in triangles.geoms:
536
- try:
537
- inter = intersection(t, constellation.boundary)
538
- except Exception:
539
- continue
540
- if (
541
- inter.geom_type == "Polygon"
542
- and len(list(zip(*inter.exterior.coords.xy))) > 2
543
- ):
544
- polygons.append(inter)
545
-
546
- p_by_area = {pg.area: pg for pg in polygons}
547
- polygons_sorted = [
548
- p_by_area[k] for k in sorted(p_by_area.keys(), reverse=True)
549
- ]
550
-
551
- # sort by combination of horizontal extent and area
552
- polygons_sorted = sorted(polygons_sorted, key=sorter, reverse=True)
553
-
554
- if len(polygons_sorted) > 0:
555
- i = 0
556
- ra, dec = polygons_sorted[i].centroid.x, polygons_sorted[i].centroid.y
557
- else:
558
- ra, dec = constellation.ra, constellation.dec
559
-
560
- text = labels.get(constellation.iau_id)
561
- style = style or self.style.constellation.label
562
- self.text(text, ra, dec, style)
563
-
564
- @use_style(PolygonStyle, "milky_way")
565
- def milky_way(self, style: PolygonStyle = None):
566
- """Plots the Milky Way
567
-
568
- Args:
569
- style: Styling of the Milky Way. If None, then the plot's style (specified when creating the plot) will be used
570
- """
571
- mw = self._read_geo_package(DataFiles.MILKY_WAY.value)
572
-
573
- if mw.empty:
574
- return
575
-
576
- def _prepare_polygon(p):
577
- points = list(zip(*p.boundary.coords.xy))
578
- # convert lon to RA and reverse so the coordinates are counterclockwise order
579
- return [(lon_to_ra(lon) * 15, dec) for lon, dec in reversed(points)]
580
-
581
- # create union of all Milky Way patches
582
- gs = mw.geometry.to_crs(self._plate_carree)
583
- mw_union = gs.buffer(0.1).unary_union.buffer(-0.1)
584
- polygons = []
585
-
586
- if mw_union.geom_type == "MultiPolygon":
587
- polygons.extend([_prepare_polygon(polygon) for polygon in mw_union.geoms])
588
- else:
589
- polygons.append(_prepare_polygon(mw_union))
590
-
591
- for polygon_points in polygons:
592
- self._polygon(
593
- polygon_points,
594
- style=style,
595
- )
596
-
597
221
  @use_style(ObjectStyle, "zenith")
598
222
  def zenith(
599
223
  self,
@@ -618,7 +242,7 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
618
242
  ra, dec, _ = zenith.radec()
619
243
 
620
244
  self.marker(
621
- ra=ra.hours,
245
+ ra=ra.hours * 15,
622
246
  dec=dec.degrees,
623
247
  style=style,
624
248
  label=label,
@@ -647,44 +271,60 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
647
271
  ra, dec, _ = zenith.radec()
648
272
 
649
273
  points = geod.ellipse(
650
- center=(ra.hours, dec.degrees),
274
+ center=(ra.hours * 15, dec.degrees),
651
275
  height_degrees=180,
652
276
  width_degrees=180,
653
277
  num_pts=100,
654
278
  )
655
279
  x = []
656
280
  y = []
657
- verts = []
658
-
659
- # TODO : handle map edges better
660
281
 
661
282
  for ra, dec in points:
662
- ra = ra / 15
663
283
  x0, y0 = self._prepare_coords(ra, dec)
664
284
  x.append(x0)
665
285
  y.append(y0)
666
- verts.append((x0, y0))
667
286
 
668
287
  style_kwargs = {}
669
288
  if self.projection == Projection.ZENITH:
670
289
  """
671
- For zenith projections, we plot the horizon as a patch because
672
- plottting as a line results in extra pixels on bottom.
673
-
674
- TODO : investigate why line is extra thick on bottom when plotting line
290
+ For zenith projections, we plot the horizon as a patch to make a more perfect circle
675
291
  """
676
292
  style_kwargs = style.line.matplot_kwargs(self.scale)
677
293
  style_kwargs["clip_on"] = False
678
294
  style_kwargs["edgecolor"] = style_kwargs.pop("color")
679
-
680
- patch = patches.Polygon(
681
- verts,
295
+ patch = patches.Circle(
296
+ (0.50, 0.50),
297
+ radius=0.454,
682
298
  facecolor=None,
683
299
  fill=False,
684
- transform=self._crs,
300
+ transform=self.ax.transAxes,
685
301
  **style_kwargs,
686
302
  )
687
303
  self.ax.add_patch(patch)
304
+ self._background_clip_path = patch
305
+ self._update_clip_path_polygon(
306
+ buffer=style.line.width / 2 + 2 * style.line.edge_width + 20
307
+ )
308
+
309
+ if not labels:
310
+ return
311
+
312
+ label_ax_coords = [
313
+ (0.5, 0.95), # north
314
+ (0.045, 0.5), # east
315
+ (0.5, 0.045), # south
316
+ (0.954, 0.5), # west
317
+ ]
318
+ for label, coords in zip(labels, label_ax_coords):
319
+ self.ax.annotate(
320
+ label,
321
+ coords,
322
+ xycoords=self.ax.transAxes,
323
+ clip_on=False,
324
+ **style.label.matplot_kwargs(self.scale),
325
+ )
326
+
327
+ return
688
328
 
689
329
  else:
690
330
  style_kwargs["clip_on"] = True
@@ -698,13 +338,6 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
698
338
  **self._plot_kwargs(),
699
339
  )
700
340
 
701
- # self.circle(
702
- # (ra.hours, dec.degrees),
703
- # 90,
704
- # style,
705
- # num_pts=200,
706
- # )
707
-
708
341
  if not labels:
709
342
  return
710
343
 
@@ -717,21 +350,19 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
717
350
 
718
351
  text_kwargs = dict(
719
352
  **style.label.matplot_kwargs(self.scale),
720
- hide_on_collision=False,
721
353
  xytext=(
722
354
  style.label.offset_x * self.scale,
723
355
  style.label.offset_y * self.scale,
724
356
  ),
725
357
  textcoords="offset points",
726
358
  path_effects=[],
359
+ clip_on=True,
727
360
  )
728
361
 
729
- if self.projection == Projection.ZENITH:
730
- text_kwargs["clip_on"] = False
731
-
732
362
  for i, position in enumerate(cardinal_directions):
733
363
  ra, dec, _ = position.radec()
734
- self._text(ra.hours, dec.degrees, labels[i], force=True, **text_kwargs)
364
+ x, y = self._prepare_coords(ra.hours * 15, dec.degrees)
365
+ self._text(x, y, labels[i], **text_kwargs)
735
366
 
736
367
  @use_style(PathStyle, "gridlines")
737
368
  def gridlines(
@@ -751,12 +382,12 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
751
382
  Args:
752
383
  style: Styling of the gridlines. If None, then the plot's style (specified when creating the plot) will be used
753
384
  labels: If True, then labels for each gridline will be plotted on the outside of the axes.
754
- ra_locations: List of Right Ascension locations for the gridlines (in hours, 0...24). Defaults to every 1 hour.
385
+ ra_locations: List of Right Ascension locations for the gridlines (in degrees, 0...360). Defaults to every 15 degrees.
755
386
  dec_locations: List of Declination locations for the gridlines (in degrees, -90...90). Defaults to every 10 degrees.
756
387
  ra_formatter_fn: Callable for creating labels of right ascension gridlines
757
388
  dec_formatter_fn: Callable for creating labels of declination gridlines
758
389
  tick_marks: If True, then tick marks will be plotted outside the axis. **Only supported for rectangular projections (e.g. Mercator, Miller)**
759
- ra_tick_locations: List of Right Ascension locations for the tick marks (in hours, 0...24)
390
+ ra_tick_locations: List of Right Ascension locations for the tick marks (in degrees, 0...260)
760
391
  dec_tick_locations: List of Declination locations for the tick marks (in degrees, -90...90)
761
392
  """
762
393
 
@@ -773,7 +404,7 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
773
404
  def dec_formatter(x, pos) -> str:
774
405
  return dec_formatter_fn(x)
775
406
 
776
- ra_locations = ra_locations or [x for x in range(24)]
407
+ ra_locations = ra_locations or [x for x in range(0, 360, 15)]
777
408
  dec_locations = dec_locations or [d for d in range(-80, 90, 10)]
778
409
 
779
410
  line_style_kwargs = style.line.matplot_kwargs()
@@ -802,14 +433,14 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
802
433
  # because cartopy does not extend lines to poles
803
434
  for ra in ra_locations:
804
435
  self.ax.plot(
805
- (ra * 15, ra * 15),
436
+ (ra, ra),
806
437
  (-90, 90),
807
438
  gid="gridlines",
808
439
  **line_style_kwargs,
809
440
  **self._plot_kwargs(),
810
441
  )
811
442
 
812
- gridlines.xlocator = FixedLocator([ra_to_lon(r) for r in ra_locations])
443
+ gridlines.xlocator = FixedLocator([ra_to_lon(r / 15) for r in ra_locations])
813
444
  gridlines.xformatter = FuncFormatter(ra_formatter)
814
445
  gridlines.xlabel_style = label_style_kwargs
815
446
 
@@ -824,10 +455,10 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
824
455
  def in_axes(ra):
825
456
  return self.in_bounds(ra, (self.dec_max + self.dec_min) / 2)
826
457
 
827
- xticks = ra_tick_locations or [x for x in np.arange(0, 24, 0.125)]
458
+ xticks = ra_tick_locations or [x for x in np.arange(0, 360, 1.875)]
828
459
  yticks = dec_tick_locations or [x for x in np.arange(-90, 90, 1)]
829
460
 
830
- inbound_xticks = [ra_to_lon(ra) for ra in xticks if in_axes(ra)]
461
+ inbound_xticks = [ra_to_lon(ra / 15) for ra in xticks if in_axes(ra)]
831
462
  self.ax.set_xticks(inbound_xticks, crs=self._plate_carree)
832
463
  self.ax.xaxis.set_major_formatter(ticker.NullFormatter())
833
464
 
@@ -869,7 +500,7 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
869
500
  Projection.STEREOGRAPHIC,
870
501
  Projection.ZENITH,
871
502
  ]:
872
- # Calculate LST to shift RA DEC to be in line with current date and time
503
+ # Calculate local sidereal time (LST) to shift RA DEC to be in line with current date and time
873
504
  lst = -(360.0 * self.timescale.gmst / 24.0 + self.lon) % 360.0
874
505
  self._proj = Projection.crs(self.projection, lon=lst, lat=self.lat)
875
506
  elif self.projection == Projection.LAMBERT_AZ_EQ_AREA:
@@ -901,9 +532,8 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
901
532
 
902
533
  self.logger.debug(f"Projection = {self.projection.value.upper()}")
903
534
 
904
- self._plot_background_clip_path()
905
-
906
535
  self._fit_to_ax()
536
+ self._plot_background_clip_path()
907
537
 
908
538
  @use_style(LabelStyle, "info_text")
909
539
  def info(self, style: LabelStyle = None):
@@ -928,12 +558,18 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
928
558
  **style.matplot_kwargs(self.scale),
929
559
  )
930
560
 
561
+ def _ax_to_radec(self, x, y):
562
+ trans = self.ax.transAxes + self.ax.transData.inverted()
563
+ x_projected, y_projected = trans.transform((x, y)) # axes to data
564
+ x_ra, y_ra = self._crs.transform_point(x_projected, y_projected, self._proj)
565
+ return (x_ra + 360), y_ra
566
+
931
567
  def _plot_background_clip_path(self):
932
568
  def to_axes(points):
933
569
  ax_points = []
934
570
 
935
571
  for ra, dec in points:
936
- x, y = self._proj.transform_point(ra * 15, dec, self._crs)
572
+ x, y = self._proj.transform_point(ra, dec, self._crs)
937
573
  data_to_axes = self.ax.transData + self.ax.transAxes.inverted()
938
574
  x_axes, y_axes = data_to_axes.transform((x, y))
939
575
  ax_points.append([x_axes, y_axes])
@@ -974,3 +610,4 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
974
610
  )
975
611
 
976
612
  self.ax.add_patch(self._background_clip_path)
613
+ self._update_clip_path_polygon()