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.
- starplot/__init__.py +3 -2
- starplot/base.py +408 -95
- starplot/callables.py +61 -7
- starplot/coordinates.py +6 -0
- starplot/data/bayer.py +1532 -3
- starplot/data/constellations.py +564 -2
- starplot/data/flamsteed.py +2682 -0
- starplot/data/library/constellation_borders_inv.gpkg +0 -0
- starplot/data/library/constellation_lines_hips.json +3 -1
- starplot/data/stars.py +408 -87
- starplot/geometry.py +82 -0
- starplot/horizon.py +458 -0
- starplot/map.py +97 -284
- starplot/models/base.py +9 -2
- starplot/models/constellation.py +1 -1
- starplot/optic.py +32 -14
- starplot/plotters/__init__.py +2 -0
- starplot/plotters/constellations.py +339 -0
- starplot/plotters/dsos.py +5 -1
- starplot/plotters/experimental.py +171 -0
- starplot/plotters/milkyway.py +41 -0
- starplot/plotters/stars.py +143 -13
- starplot/styles/base.py +308 -169
- starplot/styles/ext/antique.yml +54 -46
- starplot/styles/ext/blue_dark.yml +39 -45
- starplot/styles/ext/blue_light.yml +49 -30
- starplot/styles/ext/blue_medium.yml +53 -50
- starplot/styles/ext/cb_wong.yml +16 -7
- starplot/styles/ext/grayscale.yml +17 -10
- starplot/styles/ext/grayscale_dark.yml +18 -8
- starplot/styles/ext/map.yml +10 -7
- starplot/styles/ext/nord.yml +38 -38
- starplot/styles/ext/optic.yml +7 -5
- starplot/styles/fonts-library/gfs-didot/DESCRIPTION.en_us.html +9 -0
- starplot/styles/fonts-library/gfs-didot/GFSDidot-Regular.ttf +0 -0
- starplot/styles/fonts-library/gfs-didot/METADATA.pb +16 -0
- starplot/styles/fonts-library/gfs-didot/OFL.txt +94 -0
- starplot/styles/fonts-library/hind/DESCRIPTION.en_us.html +28 -0
- starplot/styles/fonts-library/hind/Hind-Bold.ttf +0 -0
- starplot/styles/fonts-library/hind/Hind-Light.ttf +0 -0
- starplot/styles/fonts-library/hind/Hind-Medium.ttf +0 -0
- starplot/styles/fonts-library/hind/Hind-Regular.ttf +0 -0
- starplot/styles/fonts-library/hind/Hind-SemiBold.ttf +0 -0
- starplot/styles/fonts-library/hind/METADATA.pb +58 -0
- starplot/styles/fonts-library/hind/OFL.txt +93 -0
- starplot/styles/fonts-library/inter/Inter-Black.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-BlackItalic.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-Bold.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-BoldItalic.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-ExtraBold.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-ExtraBoldItalic.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-ExtraLight.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-ExtraLightItalic.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-Italic.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-Light.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-LightItalic.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-Medium.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-MediumItalic.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-Regular.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-SemiBold.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-SemiBoldItalic.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-Thin.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-ThinItalic.ttf +0 -0
- starplot/styles/fonts-library/inter/LICENSE.txt +92 -0
- starplot/styles/fonts.py +15 -0
- starplot/styles/markers.py +207 -6
- starplot/utils.py +19 -0
- starplot/warnings.py +16 -0
- {starplot-0.12.5.dist-info → starplot-0.14.0.dist-info}/METADATA +12 -12
- starplot-0.14.0.dist-info/RECORD +107 -0
- starplot-0.12.5.dist-info/RECORD +0 -67
- {starplot-0.12.5.dist-info → starplot-0.14.0.dist-info}/LICENSE +0 -0
- {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 =
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
233
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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)
|
starplot/plotters/__init__.py
CHANGED
|
@@ -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(
|
|
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
|
+
)
|