starplot 0.20.5__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.
- starplot/__init__.py +59 -0
- starplot/callables.py +176 -0
- starplot/cli.py +29 -0
- starplot/config.py +80 -0
- starplot/coordinates.py +7 -0
- starplot/data/__init__.py +22 -0
- starplot/data/catalogs.py +356 -0
- starplot/data/constellations.py +71 -0
- starplot/data/db.py +33 -0
- starplot/data/dsos.py +85 -0
- starplot/data/ecliptic.py +201 -0
- starplot/data/library/constellation_names.parquet +0 -0
- starplot/data/library/dso_names.parquet +0 -0
- starplot/data/library/readme.md +1 -0
- starplot/data/library/star_designations.parquet +0 -0
- starplot/data/stars.py +79 -0
- starplot/data/translations.py +408 -0
- starplot/data/utils.py +35 -0
- starplot/geometry.py +319 -0
- starplot/mixins.py +360 -0
- starplot/models/__init__.py +21 -0
- starplot/models/base.py +77 -0
- starplot/models/comet.py +302 -0
- starplot/models/constellation.py +151 -0
- starplot/models/dso.py +317 -0
- starplot/models/milky_way.py +30 -0
- starplot/models/moon.py +130 -0
- starplot/models/objects.py +29 -0
- starplot/models/observer.py +125 -0
- starplot/models/optics.py +342 -0
- starplot/models/planet.py +137 -0
- starplot/models/satellite.py +138 -0
- starplot/models/star.py +253 -0
- starplot/models/sun.py +62 -0
- starplot/plots/__init__.py +7 -0
- starplot/plots/base.py +1023 -0
- starplot/plots/galaxy.py +372 -0
- starplot/plots/horizon.py +543 -0
- starplot/plots/map.py +515 -0
- starplot/plots/optic.py +469 -0
- starplot/plots/zenith.py +217 -0
- starplot/plotters/__init__.py +9 -0
- starplot/plotters/arrow.py +174 -0
- starplot/plotters/constellations.py +298 -0
- starplot/plotters/debug.py +21 -0
- starplot/plotters/dsos.py +294 -0
- starplot/plotters/experimental.py +722 -0
- starplot/plotters/gradients.py +153 -0
- starplot/plotters/legend.py +253 -0
- starplot/plotters/milkyway.py +51 -0
- starplot/plotters/stars.py +319 -0
- starplot/plotters/text.py +802 -0
- starplot/profile.py +16 -0
- starplot/projections.py +184 -0
- starplot/styles/__init__.py +6 -0
- starplot/styles/base.py +1344 -0
- starplot/styles/ext/antique.yml +175 -0
- starplot/styles/ext/blue_dark.yml +163 -0
- starplot/styles/ext/blue_gold.yml +147 -0
- starplot/styles/ext/blue_light.yml +123 -0
- starplot/styles/ext/blue_medium.yml +142 -0
- starplot/styles/ext/blue_night.yml +185 -0
- starplot/styles/ext/cb_wong.yml +124 -0
- starplot/styles/ext/color_print.yml +111 -0
- starplot/styles/ext/gradient_presets.yml +158 -0
- starplot/styles/ext/grayscale.yml +94 -0
- starplot/styles/ext/grayscale_dark.yml +136 -0
- starplot/styles/ext/map.yml +12 -0
- starplot/styles/ext/nord.yml +158 -0
- starplot/styles/ext/optic.yml +20 -0
- starplot/styles/ext/publication.yml +8 -0
- starplot/styles/extensions.py +129 -0
- 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/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-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-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/LICENSE.txt +92 -0
- starplot/styles/fonts.py +15 -0
- starplot/styles/helpers.py +93 -0
- starplot/styles/markers.py +308 -0
- starplot/utils.py +169 -0
- starplot/warnings.py +21 -0
- starplot-0.20.5.dist-info/METADATA +146 -0
- starplot-0.20.5.dist-info/RECORD +98 -0
- starplot-0.20.5.dist-info/WHEEL +4 -0
- starplot-0.20.5.dist-info/entry_points.txt +3 -0
- starplot-0.20.5.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import random
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from matplotlib import patches
|
|
6
|
+
from shapely import LineString, Polygon, affinity, ops
|
|
7
|
+
|
|
8
|
+
from starplot.geometry import circle
|
|
9
|
+
from starplot.profile import profile
|
|
10
|
+
from starplot.coordinates import CoordinateSystem
|
|
11
|
+
from starplot.styles import ArrowStyle
|
|
12
|
+
from starplot.styles.helpers import use_style
|
|
13
|
+
from starplot.utils import points_on_line
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ArrowPlotterMixin:
|
|
17
|
+
def _to_axes(self, points):
|
|
18
|
+
ax_points = []
|
|
19
|
+
|
|
20
|
+
for ra, dec in points:
|
|
21
|
+
x, y = self._proj.transform_point(ra, dec, self._crs)
|
|
22
|
+
data_to_axes = self.ax.transData + self.ax.transAxes.inverted()
|
|
23
|
+
x_axes, y_axes = data_to_axes.transform((x, y))
|
|
24
|
+
ax_points.append([x_axes, y_axes])
|
|
25
|
+
return ax_points
|
|
26
|
+
|
|
27
|
+
def _to_display(self, points):
|
|
28
|
+
display_points = []
|
|
29
|
+
|
|
30
|
+
for ra, dec in points:
|
|
31
|
+
if self._coordinate_system == CoordinateSystem.RA_DEC:
|
|
32
|
+
ra *= -1
|
|
33
|
+
|
|
34
|
+
data_x, data_y = self._proj.transform_point(ra, dec, self._geodetic)
|
|
35
|
+
if np.isnan(data_x) or np.isnan(data_y):
|
|
36
|
+
continue
|
|
37
|
+
|
|
38
|
+
display_x, display_y = self.ax.transData.transform((data_x, data_y))
|
|
39
|
+
display_points.append((display_x, display_y))
|
|
40
|
+
|
|
41
|
+
return display_points
|
|
42
|
+
|
|
43
|
+
@profile
|
|
44
|
+
@use_style(ArrowStyle, "arrow")
|
|
45
|
+
def arrow(
|
|
46
|
+
self,
|
|
47
|
+
origin: tuple[float, float] = None,
|
|
48
|
+
target: tuple[float, float] = None,
|
|
49
|
+
style: ArrowStyle = None,
|
|
50
|
+
scale: float = 0.99,
|
|
51
|
+
length: float = 5,
|
|
52
|
+
max_attempts: int = 100,
|
|
53
|
+
):
|
|
54
|
+
"""
|
|
55
|
+
Plots an arrow from one point to another if you specify both `origin` and `target`. If you only specify a `target`, then the arrow will serve as a "pointer" to that target.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
origin: Starting point (ra, dec)
|
|
59
|
+
target: Target of the arrow (ra, dec)
|
|
60
|
+
style: Style of the arrow
|
|
61
|
+
scale: Scaling factor for the arrow, to make it offset from the origin/target
|
|
62
|
+
length: If you only specify a target, then this will be the length of the arrow (in degrees). This value is ignored if you're plotting an arrow from one point to another.
|
|
63
|
+
max_attempts: If you only specify a target, then this will be the max number of attempts for plotting the arrow without colliding with labels. Arrow will be plotted on the final attempt.
|
|
64
|
+
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def create_arrow(p0, p1):
|
|
68
|
+
ax_coords = self._to_axes([origin, target])
|
|
69
|
+
x, y = ax_coords[0]
|
|
70
|
+
target_x, target_y = ax_coords[1]
|
|
71
|
+
|
|
72
|
+
# angle to rotate head
|
|
73
|
+
angle_radians = np.atan2(target_y - y, target_x - x)
|
|
74
|
+
angle_degrees = np.degrees(angle_radians)
|
|
75
|
+
|
|
76
|
+
line_x = [
|
|
77
|
+
(1 - scale) * ax_coords[1][0] + scale * ax_coords[0][0],
|
|
78
|
+
(1 - scale) * ax_coords[0][0] + scale * ax_coords[1][0],
|
|
79
|
+
]
|
|
80
|
+
line_y = [
|
|
81
|
+
(1 - scale) * ax_coords[1][1] + scale * ax_coords[0][1],
|
|
82
|
+
(1 - scale) * ax_coords[0][1] + scale * ax_coords[1][1],
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
arrow_x = line_x[-1]
|
|
86
|
+
arrow_y = line_y[-1]
|
|
87
|
+
|
|
88
|
+
body_width = style.body_width / self.resolution * 0.5
|
|
89
|
+
head_width = style.head_width / self.resolution * 0.5
|
|
90
|
+
head_height = style.head_height / self.resolution * 0.5
|
|
91
|
+
|
|
92
|
+
arrow_line = LineString(list(zip(line_x, line_y)))
|
|
93
|
+
arrow_body = arrow_line.buffer(body_width, **style.shapely_kwargs())
|
|
94
|
+
|
|
95
|
+
arrow_head = Polygon(
|
|
96
|
+
[
|
|
97
|
+
(arrow_x, arrow_y),
|
|
98
|
+
(arrow_x + head_width, arrow_y - head_height),
|
|
99
|
+
(arrow_x - head_width, arrow_y - head_height),
|
|
100
|
+
(arrow_x, arrow_y),
|
|
101
|
+
]
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
arrow_head = affinity.rotate(
|
|
105
|
+
arrow_head,
|
|
106
|
+
angle_degrees + 270,
|
|
107
|
+
origin=(arrow_x, arrow_y),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
result = ops.split(arrow_line, arrow_head)
|
|
111
|
+
arrow_body = result.geoms[0].buffer(body_width, **style.shapely_kwargs())
|
|
112
|
+
return arrow_body.union(arrow_head.buffer(0.0001, **style.shapely_kwargs()))
|
|
113
|
+
|
|
114
|
+
if origin and target:
|
|
115
|
+
origin = self._prepare_coords(*origin)
|
|
116
|
+
target = self._prepare_coords(*target)
|
|
117
|
+
arrow_polygon = create_arrow(origin, target)
|
|
118
|
+
|
|
119
|
+
elif target:
|
|
120
|
+
# we only need to prepare the target coords because the origin will be
|
|
121
|
+
# based on the circle around the prepared target coords (so the origin will be automatically in the expected coord system)
|
|
122
|
+
target = self._prepare_coords(*target)
|
|
123
|
+
arrow_polygon = None
|
|
124
|
+
attempts = 0
|
|
125
|
+
padding = 8
|
|
126
|
+
polygon = circle(
|
|
127
|
+
center=target,
|
|
128
|
+
diameter_degrees=length * 2,
|
|
129
|
+
num_pts=200,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
origins = list(zip(*polygon.exterior.coords.xy))
|
|
133
|
+
|
|
134
|
+
while arrow_polygon is None and attempts < max_attempts:
|
|
135
|
+
attempts += 1
|
|
136
|
+
origin = random.choice(origins)
|
|
137
|
+
display_points = self._to_display([origin, target])
|
|
138
|
+
|
|
139
|
+
if len(display_points) < 2:
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
points_arrow = points_on_line(display_points[0], display_points[1], 25)
|
|
143
|
+
|
|
144
|
+
# check if arrow body collides with any labels
|
|
145
|
+
collides_with_label = False
|
|
146
|
+
for x, y in points_arrow:
|
|
147
|
+
bbox = (
|
|
148
|
+
x - padding,
|
|
149
|
+
y - padding,
|
|
150
|
+
x + padding,
|
|
151
|
+
y + padding,
|
|
152
|
+
)
|
|
153
|
+
if self._is_label_collision(bbox):
|
|
154
|
+
collides_with_label = True
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
# if arrow body does not collide with labels, we can create polygon and exit loop
|
|
158
|
+
if not collides_with_label or attempts == max_attempts:
|
|
159
|
+
arrow_polygon = create_arrow(origin, target)
|
|
160
|
+
|
|
161
|
+
else:
|
|
162
|
+
raise ValueError(
|
|
163
|
+
"To plot an arrow you must specify a target or a target and origin."
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
patch = patches.Polygon(
|
|
167
|
+
list(zip(*arrow_polygon.exterior.coords.xy)),
|
|
168
|
+
**style.matplot_kwargs(self.scale),
|
|
169
|
+
transform=self.ax.transAxes,
|
|
170
|
+
)
|
|
171
|
+
self.ax.add_patch(patch)
|
|
172
|
+
# Need to set clip path AFTER adding patch
|
|
173
|
+
patch.set_clip_on(True)
|
|
174
|
+
patch.set_clip_path(self._background_clip_path)
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
|
|
3
|
+
import rtree
|
|
4
|
+
from shapely import (
|
|
5
|
+
MultiPoint,
|
|
6
|
+
)
|
|
7
|
+
from matplotlib.collections import LineCollection
|
|
8
|
+
from ibis import _
|
|
9
|
+
|
|
10
|
+
from starplot.coordinates import CoordinateSystem
|
|
11
|
+
from starplot.data import db, constellations as condata
|
|
12
|
+
from starplot.data.catalogs import (
|
|
13
|
+
Catalog,
|
|
14
|
+
CONSTELLATIONS_IAU,
|
|
15
|
+
CONSTELLATION_BORDERS,
|
|
16
|
+
BIG_SKY_MAG11,
|
|
17
|
+
)
|
|
18
|
+
from starplot.data.stars import load as load_stars
|
|
19
|
+
from starplot.models import Star, Constellation
|
|
20
|
+
from starplot.models.constellation import from_tuple
|
|
21
|
+
from starplot.profile import profile
|
|
22
|
+
from starplot.styles import LineStyle, LabelStyle
|
|
23
|
+
from starplot.styles.helpers import use_style
|
|
24
|
+
from starplot.geometry import is_wrapped_polygon, line_segment, split_line_at_meridian
|
|
25
|
+
from starplot.plotters.text import CollisionHandler
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ConstellationPlotterMixin:
|
|
29
|
+
@profile
|
|
30
|
+
def _prepare_constellation_stars(
|
|
31
|
+
self, constellations: list[Constellation]
|
|
32
|
+
) -> dict[int, tuple[float, float]]:
|
|
33
|
+
"""
|
|
34
|
+
Returns dictionary of stars and their position:
|
|
35
|
+
|
|
36
|
+
{hip: (x,y)}
|
|
37
|
+
|
|
38
|
+
Where (x, y) is the plotted coordinate system (RA/DEC or AZ/ALT)
|
|
39
|
+
"""
|
|
40
|
+
hips = []
|
|
41
|
+
for c in constellations:
|
|
42
|
+
hips.extend(c.star_hip_ids)
|
|
43
|
+
|
|
44
|
+
results = load_stars(
|
|
45
|
+
catalog=BIG_SKY_MAG11,
|
|
46
|
+
filters=[_.hip.isin(hips)],
|
|
47
|
+
)
|
|
48
|
+
results = results.select(
|
|
49
|
+
"ra",
|
|
50
|
+
"dec",
|
|
51
|
+
"epoch_year",
|
|
52
|
+
"hip",
|
|
53
|
+
"parallax_mas",
|
|
54
|
+
"ra_mas_per_year",
|
|
55
|
+
"dec_mas_per_year",
|
|
56
|
+
)
|
|
57
|
+
df = results.to_pandas()
|
|
58
|
+
df["ra_hours"], df["dec_degrees"] = (df.ra / 15, df.dec)
|
|
59
|
+
df = self._prepare_star_coords(df, limit_by_altaz=False)
|
|
60
|
+
|
|
61
|
+
return {star.hip: (star.x, star.y) for star in df.itertuples()}
|
|
62
|
+
|
|
63
|
+
@profile
|
|
64
|
+
@use_style(LineStyle, "constellation_lines")
|
|
65
|
+
def constellations(
|
|
66
|
+
self,
|
|
67
|
+
style: LineStyle = None,
|
|
68
|
+
where: list = None,
|
|
69
|
+
sql: str = None,
|
|
70
|
+
catalog: Catalog = CONSTELLATIONS_IAU,
|
|
71
|
+
):
|
|
72
|
+
"""Plots the constellation lines **only**. To plot constellation borders and/or labels, see separate functions for them.
|
|
73
|
+
|
|
74
|
+
**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).
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
style: Styling of the constellations. If None, then the plot's style (specified when creating the plot) will be used
|
|
78
|
+
where: A list of expressions that determine which constellations to plot. See [Selecting Objects](/reference-selecting-objects/) for details.
|
|
79
|
+
sql: SQL query for selecting constellations (table name is `_`). This query will be applied _after_ any filters in the `where` kwarg.
|
|
80
|
+
catalog: The catalog of constellations to use -- see [catalogs overview](/data/overview/) for details
|
|
81
|
+
"""
|
|
82
|
+
self.logger.debug("Plotting constellation lines...")
|
|
83
|
+
|
|
84
|
+
where = where or []
|
|
85
|
+
ctr = 0
|
|
86
|
+
|
|
87
|
+
extent = self._extent_mask()
|
|
88
|
+
results = condata.load(extent=extent, filters=where, sql=sql, catalog=catalog)
|
|
89
|
+
constellations_df = results.to_pandas()
|
|
90
|
+
|
|
91
|
+
if constellations_df.empty:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
constellations = [from_tuple(c) for c in constellations_df.itertuples()]
|
|
95
|
+
|
|
96
|
+
constellation_points_to_index = []
|
|
97
|
+
lines = []
|
|
98
|
+
constars = self._prepare_constellation_stars(constellations)
|
|
99
|
+
|
|
100
|
+
for c in constellations:
|
|
101
|
+
hiplines = c.star_hip_lines
|
|
102
|
+
inbounds = False
|
|
103
|
+
|
|
104
|
+
for s1_hip, s2_hip in hiplines:
|
|
105
|
+
if not constars.get(s1_hip) or not constars.get(s2_hip):
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
x1, y1 = constars.get(s1_hip)
|
|
109
|
+
x2, y2 = constars.get(s2_hip)
|
|
110
|
+
|
|
111
|
+
if x1 == x2 and y1 == y2:
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
if x1 - x2 > 60:
|
|
115
|
+
x2 += 360
|
|
116
|
+
elif x2 - x1 > 60:
|
|
117
|
+
x1 += 360
|
|
118
|
+
|
|
119
|
+
if not inbounds and (
|
|
120
|
+
self._in_bounds_xy(x1, y1) or self._in_bounds_xy(x2, y2)
|
|
121
|
+
):
|
|
122
|
+
inbounds = True
|
|
123
|
+
elif not inbounds:
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
xy_lines = []
|
|
127
|
+
|
|
128
|
+
if x2 > 360:
|
|
129
|
+
xy_lines = [*split_line_at_meridian((x1, y1), (x2, y2))]
|
|
130
|
+
elif x1 > 360:
|
|
131
|
+
xy_lines = [*split_line_at_meridian((x2, y2), (x1, y1))]
|
|
132
|
+
else:
|
|
133
|
+
xy_lines = [[(x1, y1), (x2, y2)]]
|
|
134
|
+
|
|
135
|
+
data_lines = [
|
|
136
|
+
[self._proj.transform_point(x, y, self._crs) for x, y in line]
|
|
137
|
+
for line in xy_lines
|
|
138
|
+
]
|
|
139
|
+
display_lines = [
|
|
140
|
+
[self.ax.transData.transform(p) for p in line]
|
|
141
|
+
for line in data_lines
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
lines.extend(data_lines)
|
|
145
|
+
|
|
146
|
+
radius = style.width * self.scale if style.width else 1
|
|
147
|
+
|
|
148
|
+
for display_start, display_end in display_lines:
|
|
149
|
+
for x, y in line_segment(display_start, display_end, radius * 4):
|
|
150
|
+
if x < 0 or y < 0:
|
|
151
|
+
continue
|
|
152
|
+
bbox = (
|
|
153
|
+
x - radius,
|
|
154
|
+
y - radius,
|
|
155
|
+
x + radius,
|
|
156
|
+
y + radius,
|
|
157
|
+
)
|
|
158
|
+
if self.debug_text:
|
|
159
|
+
self._debug_bbox(bbox, color="#39FF14", width=0.5)
|
|
160
|
+
|
|
161
|
+
constellation_points_to_index.append((ctr, bbox, None))
|
|
162
|
+
ctr += 1
|
|
163
|
+
|
|
164
|
+
if inbounds:
|
|
165
|
+
self._objects.constellations.append(c)
|
|
166
|
+
|
|
167
|
+
line_collection = LineCollection(
|
|
168
|
+
lines,
|
|
169
|
+
clip_on=True,
|
|
170
|
+
clip_path=self._background_clip_path,
|
|
171
|
+
gid="constellations-line",
|
|
172
|
+
**style.matplot_line_collection_kwargs(self.scale),
|
|
173
|
+
)
|
|
174
|
+
self.ax.add_collection(line_collection)
|
|
175
|
+
|
|
176
|
+
if self._constellations_rtree.get_size() == 0:
|
|
177
|
+
self._constellations_rtree = rtree.index.Index(
|
|
178
|
+
constellation_points_to_index
|
|
179
|
+
)
|
|
180
|
+
else:
|
|
181
|
+
for bbox in constellation_points_to_index:
|
|
182
|
+
self._constellations_rtree.insert(
|
|
183
|
+
0,
|
|
184
|
+
bbox,
|
|
185
|
+
None,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
@profile
|
|
189
|
+
@use_style(LineStyle, "constellation_borders")
|
|
190
|
+
def constellation_borders(
|
|
191
|
+
self, style: LineStyle = None, catalog: Catalog = CONSTELLATION_BORDERS
|
|
192
|
+
):
|
|
193
|
+
"""Plots the constellation borders
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
style: Styling of the constellation borders. If None, then the plot's style (specified when creating the plot) will be used
|
|
197
|
+
catalog: Catalog to use for constellation borders
|
|
198
|
+
"""
|
|
199
|
+
con = db.connect()
|
|
200
|
+
borders = catalog._load(connection=con, table_name="constellation_borders")
|
|
201
|
+
borders = borders.mutate(
|
|
202
|
+
geometry=_.geometry.cast("geometry"), # cast WKB to geometry type
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
extent = self._extent_mask()
|
|
206
|
+
borders_df = borders.filter(_.geometry.intersects(extent)).to_pandas()
|
|
207
|
+
|
|
208
|
+
if borders_df.empty:
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
border_lines = []
|
|
212
|
+
geometries = [line.geometry for line in borders_df.itertuples()]
|
|
213
|
+
|
|
214
|
+
for ls in geometries:
|
|
215
|
+
if ls.length < 360:
|
|
216
|
+
ls = ls.segmentize(1)
|
|
217
|
+
|
|
218
|
+
xy = [c for c in ls.coords]
|
|
219
|
+
|
|
220
|
+
if self._coordinate_system == CoordinateSystem.RA_DEC:
|
|
221
|
+
border_lines.append(xy)
|
|
222
|
+
|
|
223
|
+
elif self._coordinate_system == CoordinateSystem.AZ_ALT:
|
|
224
|
+
coords = [self._prepare_coords(*p) for p in xy]
|
|
225
|
+
border_lines.append(coords)
|
|
226
|
+
|
|
227
|
+
else:
|
|
228
|
+
raise ValueError("Unrecognized coordinate system")
|
|
229
|
+
|
|
230
|
+
line_collection = LineCollection(
|
|
231
|
+
border_lines,
|
|
232
|
+
**style.matplot_line_collection_kwargs(self.scale),
|
|
233
|
+
transform=self._crs,
|
|
234
|
+
clip_on=True,
|
|
235
|
+
clip_path=self._background_clip_path,
|
|
236
|
+
gid="constellations-border",
|
|
237
|
+
)
|
|
238
|
+
self.ax.add_collection(line_collection)
|
|
239
|
+
|
|
240
|
+
@profile
|
|
241
|
+
@use_style(LabelStyle, "constellation_labels")
|
|
242
|
+
def constellation_labels(
|
|
243
|
+
self,
|
|
244
|
+
style: LabelStyle = None,
|
|
245
|
+
label_fn: Callable[[Constellation], str] = Constellation.get_label,
|
|
246
|
+
collision_handler: CollisionHandler = None,
|
|
247
|
+
):
|
|
248
|
+
"""
|
|
249
|
+
Plots constellation labels for all constellations that have been plotted. This means you must plot the constellations before plotting their labels.
|
|
250
|
+
|
|
251
|
+
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.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
style: Styling of the constellation labels. If None, then the plot's style (specified when creating the plot) will be used
|
|
255
|
+
label_fn: Callable for determining the label for each constellation. The default function returns the constellation's name in uppercase.
|
|
256
|
+
collision_handler: An instance of [CollisionHandler][starplot.CollisionHandler] that describes what to do on collisions with other labels, markers, etc. If `None`, then `CollisionHandler(allow_constellation_line_collisions=True)` will be used (**Important: this function does NOT default to the plot's collision handler, since it's the only area-based label function and collisions should be handled differently**).
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
collision_handler = collision_handler or self.area_label_handler
|
|
260
|
+
|
|
261
|
+
hips = []
|
|
262
|
+
for c in self.objects.constellations:
|
|
263
|
+
hips.extend(c.star_hip_ids)
|
|
264
|
+
|
|
265
|
+
all_constellation_stars = Star.find(where=[_.hip.isin(hips)])
|
|
266
|
+
|
|
267
|
+
for constellation in self.objects.constellations:
|
|
268
|
+
constellation_line_stars = [
|
|
269
|
+
s
|
|
270
|
+
for s in all_constellation_stars
|
|
271
|
+
if s.hip in constellation.star_hip_ids
|
|
272
|
+
]
|
|
273
|
+
if not constellation_line_stars:
|
|
274
|
+
continue
|
|
275
|
+
|
|
276
|
+
if is_wrapped_polygon(constellation.boundary):
|
|
277
|
+
starpoints = []
|
|
278
|
+
ra, dec = zip(*[(s.ra, s.dec) for s in constellation_line_stars])
|
|
279
|
+
new_ra = [r - 360 if r > 300 else r for r in ra]
|
|
280
|
+
starpoints = list(zip(new_ra, dec))
|
|
281
|
+
|
|
282
|
+
else:
|
|
283
|
+
ra, dec = zip(*[(s.ra, s.dec) for s in constellation_line_stars])
|
|
284
|
+
starpoints = list(zip(ra, dec))
|
|
285
|
+
|
|
286
|
+
points_line = MultiPoint(starpoints)
|
|
287
|
+
centroid = points_line.centroid
|
|
288
|
+
text = label_fn(constellation)
|
|
289
|
+
|
|
290
|
+
self.text(
|
|
291
|
+
text,
|
|
292
|
+
centroid.x,
|
|
293
|
+
centroid.y,
|
|
294
|
+
style,
|
|
295
|
+
area=constellation.boundary, # TODO : make this intersection with clip path
|
|
296
|
+
collision_handler=collision_handler,
|
|
297
|
+
gid="constellations-label-name",
|
|
298
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from matplotlib.patches import Rectangle
|
|
2
|
+
from matplotlib.transforms import Bbox
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DebugPlotterMixin:
|
|
6
|
+
def _debug_bbox(self, bbox, color, width=1):
|
|
7
|
+
bboxe = Bbox([[bbox[0], bbox[1]], [bbox[2], bbox[3]]])
|
|
8
|
+
bboxe = bboxe.transformed(self.ax.transAxes.inverted())
|
|
9
|
+
rect = Rectangle(
|
|
10
|
+
(bboxe.x0, bboxe.y0), # (x, y) position in display pixels
|
|
11
|
+
width=bboxe.width,
|
|
12
|
+
height=bboxe.height,
|
|
13
|
+
transform=self.ax.transAxes,
|
|
14
|
+
fill=False,
|
|
15
|
+
facecolor="none",
|
|
16
|
+
edgecolor=color,
|
|
17
|
+
linewidth=width,
|
|
18
|
+
alpha=1,
|
|
19
|
+
zorder=100_000,
|
|
20
|
+
)
|
|
21
|
+
self.ax.add_patch(rect)
|