starplot 0.13.0__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 +187 -185
- starplot/coordinates.py +6 -0
- starplot/data/constellations.py +564 -2
- starplot/geometry.py +82 -0
- starplot/horizon.py +237 -177
- starplot/map.py +57 -369
- starplot/models/base.py +9 -2
- starplot/models/constellation.py +1 -1
- starplot/optic.py +7 -1
- starplot/plotters/__init__.py +2 -0
- starplot/plotters/constellations.py +339 -0
- starplot/plotters/experimental.py +171 -0
- starplot/plotters/milkyway.py +41 -0
- starplot/plotters/stars.py +56 -27
- starplot/styles/base.py +49 -17
- starplot/styles/ext/antique.yml +11 -9
- starplot/styles/ext/blue_dark.yml +8 -10
- starplot/styles/ext/blue_light.yml +9 -8
- starplot/styles/ext/blue_medium.yml +12 -13
- starplot/styles/ext/cb_wong.yml +9 -7
- starplot/styles/ext/grayscale.yml +4 -3
- starplot/styles/ext/grayscale_dark.yml +7 -5
- starplot/styles/ext/map.yml +9 -6
- starplot/styles/ext/nord.yml +7 -7
- starplot/styles/ext/optic.yml +1 -1
- starplot/utils.py +19 -0
- starplot/warnings.py +16 -0
- {starplot-0.13.0.dist-info → starplot-0.14.0.dist-info}/METADATA +10 -9
- {starplot-0.13.0.dist-info → starplot-0.14.0.dist-info}/RECORD +32 -26
- {starplot-0.13.0.dist-info → starplot-0.14.0.dist-info}/LICENSE +0 -0
- {starplot-0.13.0.dist-info → starplot-0.14.0.dist-info}/WHEEL +0 -0
|
@@ -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)
|
|
@@ -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
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from starplot.data import DataFiles
|
|
2
|
+
from starplot.styles import PolygonStyle
|
|
3
|
+
from starplot.styles.helpers import use_style
|
|
4
|
+
from starplot.utils import lon_to_ra
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MilkyWayPlotterMixin:
|
|
8
|
+
@use_style(PolygonStyle, "milky_way")
|
|
9
|
+
def milky_way(self, style: PolygonStyle = None):
|
|
10
|
+
"""
|
|
11
|
+
Plots the Milky Way
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
style: Styling of the Milky Way. If None, then the plot's style (specified when creating the plot) will be used
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
mw = self._read_geo_package(DataFiles.MILKY_WAY.value)
|
|
18
|
+
|
|
19
|
+
if mw.empty:
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
def _prepare_polygon(p):
|
|
23
|
+
points = list(zip(*p.boundary.coords.xy))
|
|
24
|
+
# convert lon to RA and reverse so the coordinates are counterclockwise order
|
|
25
|
+
return [(lon_to_ra(lon) * 15, dec) for lon, dec in reversed(points)]
|
|
26
|
+
|
|
27
|
+
# create union of all Milky Way patches
|
|
28
|
+
gs = mw.geometry.to_crs(self._plate_carree)
|
|
29
|
+
mw_union = gs.buffer(0.1).unary_union.buffer(-0.1)
|
|
30
|
+
polygons = []
|
|
31
|
+
|
|
32
|
+
if mw_union.geom_type == "MultiPolygon":
|
|
33
|
+
polygons.extend([_prepare_polygon(polygon) for polygon in mw_union.geoms])
|
|
34
|
+
else:
|
|
35
|
+
polygons.append(_prepare_polygon(mw_union))
|
|
36
|
+
|
|
37
|
+
for polygon_points in polygons:
|
|
38
|
+
self._polygon(
|
|
39
|
+
points=polygon_points,
|
|
40
|
+
style=style,
|
|
41
|
+
)
|
starplot/plotters/stars.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
from typing import Callable, Mapping
|
|
2
2
|
from functools import cache
|
|
3
3
|
|
|
4
|
+
import rtree
|
|
4
5
|
from skyfield.api import Star as SkyfieldStar
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
import numpy as np
|
|
7
8
|
|
|
8
9
|
from starplot import callables
|
|
10
|
+
from starplot.coordinates import CoordinateSystem
|
|
9
11
|
from starplot.data import bayer, stars, flamsteed
|
|
10
12
|
from starplot.data.stars import StarCatalog, STAR_NAMES
|
|
11
13
|
from starplot.models.star import Star, from_tuple
|
|
@@ -117,8 +119,6 @@ class StarPlotterMixin:
|
|
|
117
119
|
label,
|
|
118
120
|
s.ra,
|
|
119
121
|
s.dec,
|
|
120
|
-
# style,
|
|
121
|
-
# _offset(style, star_sizes[i]),
|
|
122
122
|
style=style.label.offset_from_marker(
|
|
123
123
|
marker_symbol=style.marker.symbol,
|
|
124
124
|
marker_size=star_sizes[i],
|
|
@@ -222,6 +222,7 @@ class StarPlotterMixin:
|
|
|
222
222
|
color_fn = color_fn or (lambda d: color_hex)
|
|
223
223
|
|
|
224
224
|
where = where or []
|
|
225
|
+
stars_to_index = []
|
|
225
226
|
|
|
226
227
|
if where:
|
|
227
228
|
mag = None
|
|
@@ -242,42 +243,67 @@ class StarPlotterMixin:
|
|
|
242
243
|
self._prepare_star_coords(nearby_stars_df)
|
|
243
244
|
|
|
244
245
|
starz = []
|
|
246
|
+
ctr = 0
|
|
245
247
|
|
|
246
248
|
for star in nearby_stars_df.itertuples():
|
|
247
249
|
obj = from_tuple(star, catalog)
|
|
250
|
+
ctr += 1
|
|
248
251
|
|
|
249
252
|
if not all([e.evaluate(obj) for e in where]):
|
|
250
253
|
continue
|
|
251
254
|
|
|
255
|
+
if self._coordinate_system == CoordinateSystem.RA_DEC:
|
|
256
|
+
data_xy = self._proj.transform_point(
|
|
257
|
+
star.ra * -1, star.dec, self._geodetic
|
|
258
|
+
)
|
|
259
|
+
elif self._coordinate_system == CoordinateSystem.AZ_ALT:
|
|
260
|
+
data_xy = self._proj.transform_point(star.x, star.y, self._crs)
|
|
261
|
+
else:
|
|
262
|
+
raise ValueError("Unrecognized coordinate system")
|
|
263
|
+
|
|
264
|
+
display_x, display_y = self.ax.transData.transform(data_xy)
|
|
265
|
+
|
|
266
|
+
if (
|
|
267
|
+
display_x < 0
|
|
268
|
+
or display_y < 0
|
|
269
|
+
or np.isnan(display_x)
|
|
270
|
+
or np.isnan(display_y)
|
|
271
|
+
or self._is_clipped([(display_x, display_y)])
|
|
272
|
+
):
|
|
273
|
+
continue
|
|
274
|
+
|
|
252
275
|
size = size_fn(obj) * self.scale**2
|
|
253
276
|
alpha = alpha_fn(obj)
|
|
254
277
|
color = color_fn(obj) or style.marker.color.as_hex()
|
|
255
278
|
|
|
256
|
-
|
|
279
|
+
if obj.magnitude < 5:
|
|
280
|
+
# radius = ((size**0.5 / 2) / self.scale) #/ 3.14
|
|
281
|
+
radius = size**0.5 / 5
|
|
282
|
+
|
|
283
|
+
bbox = np.array(
|
|
284
|
+
(
|
|
285
|
+
display_x - radius,
|
|
286
|
+
display_y - radius,
|
|
287
|
+
display_x + radius,
|
|
288
|
+
display_y + radius,
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
# if obj.name == "Sirius":
|
|
292
|
+
# print(bbox)
|
|
293
|
+
# if obj.name == "Electra":
|
|
294
|
+
# print(bbox)
|
|
295
|
+
|
|
296
|
+
if self._stars_rtree.get_size() > 0:
|
|
297
|
+
self._stars_rtree.insert(
|
|
298
|
+
0,
|
|
299
|
+
bbox,
|
|
300
|
+
None,
|
|
301
|
+
)
|
|
302
|
+
else:
|
|
303
|
+
# if the index has no stars yet, then wait until end to load for better performance
|
|
304
|
+
stars_to_index.append((ctr, bbox, None))
|
|
257
305
|
|
|
258
|
-
|
|
259
|
-
# if getattr(self, "_geodetic", None):
|
|
260
|
-
# # TODO : clean up!
|
|
261
|
-
# x, y = self._proj.transform_point(
|
|
262
|
-
# star.ra * -1, star.dec, self._geodetic
|
|
263
|
-
# )
|
|
264
|
-
# x0, y0 = self.ax.transData.transform((x, y))
|
|
265
|
-
|
|
266
|
-
# if (
|
|
267
|
-
# x0 < 0
|
|
268
|
-
# or y0 < 0
|
|
269
|
-
# or obj.magnitude > 5
|
|
270
|
-
# or np.isnan(x0)
|
|
271
|
-
# or np.isnan(y0)
|
|
272
|
-
# ):
|
|
273
|
-
# continue
|
|
274
|
-
# radius = 1 + (5 - obj.magnitude)
|
|
275
|
-
# # radius = max(((size**0.5 / 2) / self.scale)/1.44 - 6, 0) #size / self.scale**2 / 200
|
|
276
|
-
# self._stars_rtree.insert(
|
|
277
|
-
# 0,
|
|
278
|
-
# np.array((x0 - radius, y0 - radius, x0 + radius, y0 + radius)),
|
|
279
|
-
# obj=star.x,
|
|
280
|
-
# )
|
|
306
|
+
starz.append((star.x, star.y, size, alpha, color, obj))
|
|
281
307
|
|
|
282
308
|
starz.sort(key=lambda s: s[2], reverse=True) # sort by descending size
|
|
283
309
|
|
|
@@ -308,6 +334,9 @@ class StarPlotterMixin:
|
|
|
308
334
|
|
|
309
335
|
self._add_legend_handle_marker(legend_label, style.marker)
|
|
310
336
|
|
|
337
|
+
if stars_to_index:
|
|
338
|
+
self._stars_rtree = rtree.index.Index(stars_to_index)
|
|
339
|
+
|
|
311
340
|
if labels:
|
|
312
341
|
self._star_labels(
|
|
313
342
|
star_objects,
|