starplot 0.18.3__py2.py3-none-any.whl → 0.19.2__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.
- starplot/__init__.py +33 -27
- starplot/config.py +11 -0
- starplot/data/__init__.py +3 -5
- starplot/data/catalogs.py +24 -12
- starplot/data/constellations.py +1 -0
- starplot/data/db.py +1 -7
- starplot/geod.py +3 -4
- starplot/geometry.py +17 -1
- starplot/mixins.py +11 -0
- starplot/models/__init__.py +3 -1
- starplot/models/constellation.py +20 -3
- starplot/models/milky_way.py +30 -0
- starplot/models/moon.py +1 -1
- starplot/models/observer.py +11 -2
- starplot/models/planet.py +1 -1
- starplot/models/sun.py +1 -1
- starplot/plots/__init__.py +6 -0
- starplot/{base.py → plots/base.py} +107 -456
- starplot/{horizon.py → plots/horizon.py} +12 -10
- starplot/{map.py → plots/map.py} +11 -7
- starplot/{optic.py → plots/optic.py} +21 -30
- starplot/{zenith.py → plots/zenith.py} +37 -8
- starplot/plotters/__init__.py +9 -7
- starplot/plotters/arrow.py +1 -1
- starplot/plotters/constellations.py +46 -61
- starplot/plotters/dsos.py +33 -16
- starplot/plotters/experimental.py +0 -1
- starplot/plotters/milkyway.py +15 -6
- starplot/plotters/stars.py +19 -36
- starplot/plotters/text.py +524 -0
- starplot/styles/__init__.py +4 -4
- starplot/styles/base.py +1 -13
- {starplot-0.18.3.dist-info → starplot-0.19.2.dist-info}/METADATA +2 -1
- {starplot-0.18.3.dist-info → starplot-0.19.2.dist-info}/RECORD +37 -35
- starplot/data/library/sky.db +0 -0
- {starplot-0.18.3.dist-info → starplot-0.19.2.dist-info}/WHEEL +0 -0
- {starplot-0.18.3.dist-info → starplot-0.19.2.dist-info}/entry_points.txt +0 -0
- {starplot-0.18.3.dist-info → starplot-0.19.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
2
|
from typing import Dict, Union, Optional
|
|
3
|
-
from random import randrange
|
|
4
3
|
import logging
|
|
5
4
|
|
|
6
5
|
import numpy as np
|
|
7
|
-
import rtree
|
|
8
6
|
from matplotlib import patches
|
|
9
7
|
from matplotlib import pyplot as plt, patheffects
|
|
10
8
|
from matplotlib.axes import Axes
|
|
11
9
|
from matplotlib.figure import Figure
|
|
12
10
|
from matplotlib.lines import Line2D
|
|
13
|
-
from shapely import Polygon,
|
|
11
|
+
from shapely import Polygon, LineString
|
|
14
12
|
|
|
15
13
|
from starplot.coordinates import CoordinateSystem
|
|
16
14
|
from starplot import geod, models, warnings
|
|
@@ -22,7 +20,6 @@ from starplot.models.moon import MoonPhase
|
|
|
22
20
|
from starplot.models.optics import Optic, Camera
|
|
23
21
|
from starplot.models.observer import Observer
|
|
24
22
|
from starplot.styles import (
|
|
25
|
-
AnchorPointEnum,
|
|
26
23
|
PlotStyle,
|
|
27
24
|
MarkerStyle,
|
|
28
25
|
ObjectStyle,
|
|
@@ -34,11 +31,8 @@ from starplot.styles import (
|
|
|
34
31
|
GradientDirection,
|
|
35
32
|
fonts,
|
|
36
33
|
)
|
|
34
|
+
from starplot.plotters.text import TextPlotterMixin, CollisionHandler
|
|
37
35
|
from starplot.styles.helpers import use_style
|
|
38
|
-
from starplot.geometry import (
|
|
39
|
-
unwrap_polygon_360,
|
|
40
|
-
random_point_in_polygon_at_distance,
|
|
41
|
-
)
|
|
42
36
|
from starplot.profile import profile
|
|
43
37
|
|
|
44
38
|
LOGGER = logging.getLogger("starplot")
|
|
@@ -49,14 +43,12 @@ LOG_FORMATTER = logging.Formatter(
|
|
|
49
43
|
LOG_HANDLER.setFormatter(LOG_FORMATTER)
|
|
50
44
|
LOGGER.addHandler(LOG_HANDLER)
|
|
51
45
|
|
|
52
|
-
DEFAULT_STYLE = PlotStyle()
|
|
53
|
-
|
|
54
46
|
DEFAULT_RESOLUTION = 4096
|
|
55
47
|
|
|
56
48
|
DPI = 100
|
|
57
49
|
|
|
58
50
|
|
|
59
|
-
class BasePlot(ABC):
|
|
51
|
+
class BasePlot(TextPlotterMixin, ABC):
|
|
60
52
|
_background_clip_path = None
|
|
61
53
|
_clip_path_polygon: Polygon = None # clip path in display coordinates
|
|
62
54
|
_coordinate_system = CoordinateSystem.RA_DEC
|
|
@@ -79,19 +71,24 @@ class BasePlot(ABC):
|
|
|
79
71
|
The plot's style.
|
|
80
72
|
"""
|
|
81
73
|
|
|
74
|
+
collision_handler: CollisionHandler
|
|
75
|
+
"""Default [collision handler][starplot.CollisionHandler] for the plot."""
|
|
76
|
+
|
|
82
77
|
def __init__(
|
|
83
78
|
self,
|
|
84
|
-
observer: Observer =
|
|
79
|
+
observer: Observer = None,
|
|
85
80
|
ephemeris: str = "de421.bsp",
|
|
86
|
-
style: PlotStyle =
|
|
81
|
+
style: PlotStyle = None,
|
|
87
82
|
resolution: int = 4096,
|
|
88
|
-
|
|
83
|
+
collision_handler: CollisionHandler = None,
|
|
89
84
|
scale: float = 1.0,
|
|
90
85
|
autoscale: bool = False,
|
|
91
86
|
suppress_warnings: bool = True,
|
|
92
87
|
*args,
|
|
93
88
|
**kwargs,
|
|
94
89
|
):
|
|
90
|
+
super().__init__(*args, **kwargs)
|
|
91
|
+
|
|
95
92
|
if StarplotSettings.svg_text_type == SvgTextType.PATH:
|
|
96
93
|
plt.rcParams["svg.fonttype"] = "path"
|
|
97
94
|
else:
|
|
@@ -102,10 +99,10 @@ class BasePlot(ABC):
|
|
|
102
99
|
|
|
103
100
|
self.language = StarplotSettings.language
|
|
104
101
|
|
|
105
|
-
self.style = style
|
|
102
|
+
self.style = style or PlotStyle()
|
|
106
103
|
self.figure_size = resolution * px
|
|
107
104
|
self.resolution = resolution
|
|
108
|
-
self.
|
|
105
|
+
self.collision_handler = collision_handler or CollisionHandler()
|
|
109
106
|
|
|
110
107
|
self.scale = scale
|
|
111
108
|
self.autoscale = autoscale
|
|
@@ -115,23 +112,19 @@ class BasePlot(ABC):
|
|
|
115
112
|
if suppress_warnings:
|
|
116
113
|
warnings.suppress()
|
|
117
114
|
|
|
118
|
-
self.observer = observer
|
|
115
|
+
self.observer = observer or Observer()
|
|
119
116
|
self._ephemeris_name = ephemeris
|
|
120
117
|
self.ephemeris = load(ephemeris)
|
|
121
118
|
self.earth = self.ephemeris["earth"]
|
|
122
119
|
|
|
123
|
-
self.labels = []
|
|
124
|
-
self._labels_rtree = rtree.index.Index()
|
|
125
|
-
self._constellations_rtree = rtree.index.Index()
|
|
126
|
-
self._stars_rtree = rtree.index.Index()
|
|
127
|
-
self._markers_rtree = rtree.index.Index()
|
|
128
|
-
|
|
129
120
|
self._background_clip_path = None
|
|
130
121
|
|
|
131
122
|
self._legend = None
|
|
132
123
|
self._legend_handles = {}
|
|
133
124
|
|
|
134
|
-
self.
|
|
125
|
+
self.debug = StarplotSettings.debug or bool(kwargs.get("debug"))
|
|
126
|
+
self.debug_text = StarplotSettings.debug or bool(kwargs.get("debug_text"))
|
|
127
|
+
self.log_level = logging.DEBUG if self.debug else logging.ERROR
|
|
135
128
|
self.logger = LOGGER
|
|
136
129
|
self.logger.setLevel(self.log_level)
|
|
137
130
|
|
|
@@ -154,127 +147,6 @@ class BasePlot(ABC):
|
|
|
154
147
|
coords = self._background_clip_path.get_verts()
|
|
155
148
|
self._clip_path_polygon = Polygon(coords).buffer(-1 * buffer)
|
|
156
149
|
|
|
157
|
-
def _is_label_collision(self, bbox) -> bool:
|
|
158
|
-
ix = list(self._labels_rtree.intersection(bbox))
|
|
159
|
-
return len(ix) > 0
|
|
160
|
-
|
|
161
|
-
def _is_constellation_collision(self, bbox) -> bool:
|
|
162
|
-
ix = list(self._constellations_rtree.intersection(bbox))
|
|
163
|
-
return len(ix) > 0
|
|
164
|
-
|
|
165
|
-
def _is_star_collision(self, bbox) -> bool:
|
|
166
|
-
ix = list(self._stars_rtree.intersection(bbox))
|
|
167
|
-
return len(ix) > 0
|
|
168
|
-
|
|
169
|
-
def _is_marker_collision(self, bbox) -> bool:
|
|
170
|
-
ix = list(self._markers_rtree.intersection(bbox))
|
|
171
|
-
return len(ix) > 0
|
|
172
|
-
|
|
173
|
-
def _is_clipped(self, points) -> bool:
|
|
174
|
-
p = self._clip_path_polygon
|
|
175
|
-
|
|
176
|
-
for x, y in points:
|
|
177
|
-
if not p.contains(Point(x, y)):
|
|
178
|
-
return True
|
|
179
|
-
|
|
180
|
-
return False
|
|
181
|
-
|
|
182
|
-
def _add_label_to_rtree(self, label, extent=None):
|
|
183
|
-
extent = extent or label.get_window_extent(
|
|
184
|
-
renderer=self.fig.canvas.get_renderer()
|
|
185
|
-
)
|
|
186
|
-
self.labels.append(label)
|
|
187
|
-
self._labels_rtree.insert(
|
|
188
|
-
0, np.array((extent.x0 - 1, extent.y0 - 1, extent.x1 + 1, extent.y1 + 1))
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
def _is_open_space(
|
|
192
|
-
self,
|
|
193
|
-
bbox: tuple[float, float, float, float],
|
|
194
|
-
padding=0,
|
|
195
|
-
avoid_clipped=True,
|
|
196
|
-
avoid_label_collisions=True,
|
|
197
|
-
avoid_marker_collisions=True,
|
|
198
|
-
avoid_constellation_collision=True,
|
|
199
|
-
) -> bool:
|
|
200
|
-
"""
|
|
201
|
-
Returns true if the boox covers an open space (i.e. no collisions)
|
|
202
|
-
|
|
203
|
-
Args:
|
|
204
|
-
bbox: 4-element tuple of lower left and upper right coordinates
|
|
205
|
-
"""
|
|
206
|
-
x0, y0, x1, y1 = bbox
|
|
207
|
-
points = [(x0, y0), (x1, y1)]
|
|
208
|
-
bbox = (
|
|
209
|
-
x0 - padding,
|
|
210
|
-
y0 - padding,
|
|
211
|
-
x1 + padding,
|
|
212
|
-
y1 + padding,
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
if any([np.isnan(c) for c in (x0, y0, x1, y1)]):
|
|
216
|
-
return False
|
|
217
|
-
|
|
218
|
-
if avoid_clipped and self._is_clipped(points):
|
|
219
|
-
return False
|
|
220
|
-
|
|
221
|
-
if avoid_label_collisions and self._is_label_collision(bbox):
|
|
222
|
-
return False
|
|
223
|
-
|
|
224
|
-
if avoid_marker_collisions and (
|
|
225
|
-
self._is_star_collision(bbox) or self._is_marker_collision(bbox)
|
|
226
|
-
):
|
|
227
|
-
return False
|
|
228
|
-
|
|
229
|
-
if avoid_constellation_collision and self._is_constellation_collision(bbox):
|
|
230
|
-
return False
|
|
231
|
-
|
|
232
|
-
return True
|
|
233
|
-
|
|
234
|
-
def _get_label_bbox(self, label):
|
|
235
|
-
extent = label.get_window_extent(renderer=self.fig.canvas.get_renderer())
|
|
236
|
-
return (extent.x0, extent.y0, extent.x1, extent.y1)
|
|
237
|
-
|
|
238
|
-
def _maybe_remove_label(
|
|
239
|
-
self,
|
|
240
|
-
label,
|
|
241
|
-
remove_on_collision=True,
|
|
242
|
-
remove_on_clipped=True,
|
|
243
|
-
remove_on_constellation_collision=True,
|
|
244
|
-
padding=0,
|
|
245
|
-
) -> bool:
|
|
246
|
-
"""Returns true if the label is removed, else false"""
|
|
247
|
-
extent = label.get_window_extent(renderer=self.fig.canvas.get_renderer())
|
|
248
|
-
bbox = (
|
|
249
|
-
extent.x0 - padding,
|
|
250
|
-
extent.y0 - padding,
|
|
251
|
-
extent.x1 + padding,
|
|
252
|
-
extent.y1 + padding,
|
|
253
|
-
)
|
|
254
|
-
points = [(extent.x0, extent.y0), (extent.x1, extent.y1)]
|
|
255
|
-
|
|
256
|
-
if any([np.isnan(c) for c in (extent.x0, extent.y0, extent.x1, extent.y1)]):
|
|
257
|
-
label.remove()
|
|
258
|
-
return True
|
|
259
|
-
|
|
260
|
-
if remove_on_clipped and self._is_clipped(points):
|
|
261
|
-
label.remove()
|
|
262
|
-
return True
|
|
263
|
-
|
|
264
|
-
if remove_on_collision and (
|
|
265
|
-
self._is_label_collision(bbox)
|
|
266
|
-
or self._is_star_collision(bbox)
|
|
267
|
-
or self._is_marker_collision(bbox)
|
|
268
|
-
):
|
|
269
|
-
label.remove()
|
|
270
|
-
return True
|
|
271
|
-
|
|
272
|
-
if remove_on_constellation_collision and self._is_constellation_collision(bbox):
|
|
273
|
-
label.remove()
|
|
274
|
-
return True
|
|
275
|
-
|
|
276
|
-
return False
|
|
277
|
-
|
|
278
150
|
def _add_legend_handle_marker(self, label: str, style: MarkerStyle):
|
|
279
151
|
if label is not None and label not in self._legend_handles:
|
|
280
152
|
s = style.matplot_kwargs()
|
|
@@ -288,235 +160,6 @@ class BasePlot(ABC):
|
|
|
288
160
|
label=label,
|
|
289
161
|
)
|
|
290
162
|
|
|
291
|
-
def _collision_score(self, label) -> int:
|
|
292
|
-
config = {
|
|
293
|
-
"labels": 1.0, # always fail
|
|
294
|
-
"stars": 0.5,
|
|
295
|
-
"constellations": 0.8,
|
|
296
|
-
"anchors": [
|
|
297
|
-
("bottom right", 0),
|
|
298
|
-
("top right", 0.2),
|
|
299
|
-
("top left", 0.5),
|
|
300
|
-
],
|
|
301
|
-
"on_fail": "plot",
|
|
302
|
-
}
|
|
303
|
-
extent = label.get_window_extent(renderer=self.fig.canvas.get_renderer())
|
|
304
|
-
|
|
305
|
-
if any(
|
|
306
|
-
[np.isnan(c) for c in (extent.x0, extent.y0, extent.x1, extent.y1)]
|
|
307
|
-
) or self._is_clipped(extent):
|
|
308
|
-
return 1
|
|
309
|
-
|
|
310
|
-
x_labels = (
|
|
311
|
-
len(
|
|
312
|
-
list(
|
|
313
|
-
self._labels_rtree.intersection(
|
|
314
|
-
(extent.x0, extent.y0, extent.x1, extent.y1)
|
|
315
|
-
)
|
|
316
|
-
)
|
|
317
|
-
)
|
|
318
|
-
* config["labels"]
|
|
319
|
-
)
|
|
320
|
-
|
|
321
|
-
if x_labels >= 1:
|
|
322
|
-
return 1
|
|
323
|
-
|
|
324
|
-
x_constellations = (
|
|
325
|
-
len(
|
|
326
|
-
list(
|
|
327
|
-
self._constellations_rtree.intersection(
|
|
328
|
-
(extent.x0, extent.y0, extent.x1, extent.y1)
|
|
329
|
-
)
|
|
330
|
-
)
|
|
331
|
-
)
|
|
332
|
-
* config["constellations"]
|
|
333
|
-
)
|
|
334
|
-
|
|
335
|
-
if x_constellations >= 1:
|
|
336
|
-
return 1
|
|
337
|
-
|
|
338
|
-
x_stars = (
|
|
339
|
-
len(
|
|
340
|
-
list(
|
|
341
|
-
self._stars_rtree.intersection(
|
|
342
|
-
(extent.x0, extent.y0, extent.x1, extent.y1)
|
|
343
|
-
)
|
|
344
|
-
)
|
|
345
|
-
)
|
|
346
|
-
* config["stars"]
|
|
347
|
-
)
|
|
348
|
-
if x_stars >= 1:
|
|
349
|
-
return 1
|
|
350
|
-
|
|
351
|
-
return sum([x_labels, x_constellations, x_stars]) / 3
|
|
352
|
-
|
|
353
|
-
def _text(self, x, y, text, **kwargs):
|
|
354
|
-
label = self.ax.annotate(
|
|
355
|
-
text,
|
|
356
|
-
(x, y),
|
|
357
|
-
**kwargs,
|
|
358
|
-
**self._plot_kwargs(),
|
|
359
|
-
)
|
|
360
|
-
if kwargs.get("clip_on"):
|
|
361
|
-
label.set_clip_on(True)
|
|
362
|
-
label.set_clip_path(self._background_clip_path)
|
|
363
|
-
return label
|
|
364
|
-
|
|
365
|
-
def _text_point(
|
|
366
|
-
self,
|
|
367
|
-
ra: float,
|
|
368
|
-
dec: float,
|
|
369
|
-
text: str,
|
|
370
|
-
hide_on_collision: bool = True,
|
|
371
|
-
force: bool = False,
|
|
372
|
-
clip_on: bool = True,
|
|
373
|
-
**kwargs,
|
|
374
|
-
):
|
|
375
|
-
if not text:
|
|
376
|
-
return None
|
|
377
|
-
|
|
378
|
-
x, y = self._prepare_coords(ra, dec)
|
|
379
|
-
|
|
380
|
-
if StarplotSettings.svg_text_type == SvgTextType.PATH:
|
|
381
|
-
kwargs["path_effects"] = kwargs.get("path_effects", [self.text_border])
|
|
382
|
-
|
|
383
|
-
remove_on_constellation_collision = kwargs.pop(
|
|
384
|
-
"remove_on_constellation_collision", True
|
|
385
|
-
)
|
|
386
|
-
|
|
387
|
-
original_va = kwargs.pop("va", None)
|
|
388
|
-
original_ha = kwargs.pop("ha", None)
|
|
389
|
-
original_offset_x, original_offset_y = kwargs.pop("xytext", (0, 0))
|
|
390
|
-
|
|
391
|
-
anchors = [(original_va, original_ha)]
|
|
392
|
-
for a in self.style.text_anchor_fallbacks:
|
|
393
|
-
d = AnchorPointEnum.from_str(a).as_matplot()
|
|
394
|
-
anchors.append((d["va"], d["ha"]))
|
|
395
|
-
|
|
396
|
-
for va, ha in anchors:
|
|
397
|
-
offset_x, offset_y = original_offset_x, original_offset_y
|
|
398
|
-
if original_ha != ha:
|
|
399
|
-
offset_x *= -1
|
|
400
|
-
|
|
401
|
-
if original_va != va:
|
|
402
|
-
offset_y *= -1
|
|
403
|
-
|
|
404
|
-
if ha == "center":
|
|
405
|
-
offset_x = 0
|
|
406
|
-
offset_y = 0
|
|
407
|
-
|
|
408
|
-
label = self._text(
|
|
409
|
-
x, y, text, **kwargs, va=va, ha=ha, xytext=(offset_x, offset_y)
|
|
410
|
-
)
|
|
411
|
-
removed = self._maybe_remove_label(
|
|
412
|
-
label,
|
|
413
|
-
remove_on_collision=hide_on_collision,
|
|
414
|
-
remove_on_clipped=clip_on,
|
|
415
|
-
remove_on_constellation_collision=remove_on_constellation_collision,
|
|
416
|
-
)
|
|
417
|
-
|
|
418
|
-
if force or not removed:
|
|
419
|
-
self._add_label_to_rtree(label)
|
|
420
|
-
return label
|
|
421
|
-
|
|
422
|
-
def _text_area(
|
|
423
|
-
self,
|
|
424
|
-
ra: float,
|
|
425
|
-
dec: float,
|
|
426
|
-
text: str,
|
|
427
|
-
area,
|
|
428
|
-
hide_on_collision: bool = True,
|
|
429
|
-
force: bool = False,
|
|
430
|
-
clip_on: bool = True,
|
|
431
|
-
settings: dict = None,
|
|
432
|
-
**kwargs,
|
|
433
|
-
) -> None:
|
|
434
|
-
kwargs["va"] = "center"
|
|
435
|
-
kwargs["ha"] = "center"
|
|
436
|
-
|
|
437
|
-
if StarplotSettings.svg_text_type == SvgTextType.PATH:
|
|
438
|
-
kwargs["path_effects"] = kwargs.get("path_effects", [self.text_border])
|
|
439
|
-
|
|
440
|
-
avoid_constellation_lines = settings.get("avoid_constellation_lines", False)
|
|
441
|
-
padding = settings.get("label_padding", 3)
|
|
442
|
-
settings.get("buffer", 0.1)
|
|
443
|
-
max_distance = settings.get("max_distance", 300)
|
|
444
|
-
distance_step_size = settings.get("distance_step_size", 1)
|
|
445
|
-
point_iterations = settings.get("point_generation_max_iterations", 500)
|
|
446
|
-
random_seed = settings.get("seed")
|
|
447
|
-
|
|
448
|
-
attempts = 0
|
|
449
|
-
height = None
|
|
450
|
-
width = None
|
|
451
|
-
bbox = None
|
|
452
|
-
areas = (
|
|
453
|
-
[p for p in area.geoms] if "MultiPolygon" == str(area.geom_type) else [area]
|
|
454
|
-
)
|
|
455
|
-
new_areas = []
|
|
456
|
-
origin = Point(ra, dec)
|
|
457
|
-
|
|
458
|
-
for a in areas:
|
|
459
|
-
unwrapped = unwrap_polygon_360(a)
|
|
460
|
-
# new_buffer = unwrapped.area / 10 * -1 * buffer * self.scale
|
|
461
|
-
# new_buffer = -1 * buffer * self.scale
|
|
462
|
-
# new_poly = unwrapped.buffer(new_buffer)
|
|
463
|
-
new_areas.append(unwrapped)
|
|
464
|
-
|
|
465
|
-
for d in range(0, max_distance, distance_step_size):
|
|
466
|
-
distance = d / 20
|
|
467
|
-
poly = randrange(len(new_areas))
|
|
468
|
-
point = random_point_in_polygon_at_distance(
|
|
469
|
-
new_areas[poly],
|
|
470
|
-
origin_point=origin,
|
|
471
|
-
distance=distance,
|
|
472
|
-
max_iterations=point_iterations,
|
|
473
|
-
seed=random_seed,
|
|
474
|
-
)
|
|
475
|
-
|
|
476
|
-
if point is None:
|
|
477
|
-
continue
|
|
478
|
-
|
|
479
|
-
x, y = self._prepare_coords(point.x, point.y)
|
|
480
|
-
|
|
481
|
-
if height and width:
|
|
482
|
-
data_xy = self._proj.transform_point(x, y, self._crs)
|
|
483
|
-
display_x, display_y = self.ax.transData.transform(data_xy)
|
|
484
|
-
bbox = (
|
|
485
|
-
display_x - width / 2,
|
|
486
|
-
display_y - height / 2,
|
|
487
|
-
display_x + width / 2,
|
|
488
|
-
display_y + height / 2,
|
|
489
|
-
)
|
|
490
|
-
label = None
|
|
491
|
-
|
|
492
|
-
else:
|
|
493
|
-
label = self._text(x, y, text, **kwargs)
|
|
494
|
-
bbox = self._get_label_bbox(label)
|
|
495
|
-
height = bbox[3] - bbox[1]
|
|
496
|
-
width = bbox[2] - bbox[0]
|
|
497
|
-
|
|
498
|
-
is_open = self._is_open_space(
|
|
499
|
-
bbox,
|
|
500
|
-
padding=padding,
|
|
501
|
-
avoid_clipped=clip_on,
|
|
502
|
-
avoid_constellation_collision=avoid_constellation_lines,
|
|
503
|
-
avoid_marker_collisions=hide_on_collision,
|
|
504
|
-
avoid_label_collisions=hide_on_collision,
|
|
505
|
-
)
|
|
506
|
-
|
|
507
|
-
# # TODO : remove label if not fully inside area?
|
|
508
|
-
|
|
509
|
-
attempts += 1
|
|
510
|
-
|
|
511
|
-
if is_open and label is None:
|
|
512
|
-
label = self._text(x, y, text, **kwargs)
|
|
513
|
-
|
|
514
|
-
if is_open:
|
|
515
|
-
self._add_label_to_rtree(label)
|
|
516
|
-
return label
|
|
517
|
-
elif label is not None:
|
|
518
|
-
label.remove()
|
|
519
|
-
|
|
520
163
|
@property
|
|
521
164
|
def magnitude_range(self) -> tuple[float, float]:
|
|
522
165
|
"""
|
|
@@ -525,64 +168,6 @@ class BasePlot(ABC):
|
|
|
525
168
|
mags = [s.magnitude for s in self.objects.stars]
|
|
526
169
|
return (min(mags), max(mags))
|
|
527
170
|
|
|
528
|
-
@use_style(LabelStyle)
|
|
529
|
-
def text(
|
|
530
|
-
self,
|
|
531
|
-
text: str,
|
|
532
|
-
ra: float,
|
|
533
|
-
dec: float,
|
|
534
|
-
style: LabelStyle = None,
|
|
535
|
-
hide_on_collision: bool = True,
|
|
536
|
-
**kwargs,
|
|
537
|
-
):
|
|
538
|
-
"""
|
|
539
|
-
Plots text
|
|
540
|
-
|
|
541
|
-
Args:
|
|
542
|
-
text: Text to plot
|
|
543
|
-
ra: Right ascension of text (0...360)
|
|
544
|
-
dec: Declination of text (-90...90)
|
|
545
|
-
style: Styling of the text
|
|
546
|
-
hide_on_collision: If True, then the text will not be plotted if it collides with another label
|
|
547
|
-
"""
|
|
548
|
-
if not text:
|
|
549
|
-
return
|
|
550
|
-
|
|
551
|
-
style = style or LabelStyle()
|
|
552
|
-
|
|
553
|
-
if style.offset_x == "auto":
|
|
554
|
-
style.offset_x = 0
|
|
555
|
-
|
|
556
|
-
if style.offset_y == "auto":
|
|
557
|
-
style.offset_y = 0
|
|
558
|
-
|
|
559
|
-
if kwargs.get("area"):
|
|
560
|
-
return self._text_area(
|
|
561
|
-
ra,
|
|
562
|
-
dec,
|
|
563
|
-
text,
|
|
564
|
-
**style.matplot_kwargs(self.scale),
|
|
565
|
-
area=kwargs.pop("area"),
|
|
566
|
-
hide_on_collision=hide_on_collision,
|
|
567
|
-
xycoords="data",
|
|
568
|
-
xytext=(style.offset_x * self.scale, style.offset_y * self.scale),
|
|
569
|
-
textcoords="offset points",
|
|
570
|
-
settings=kwargs.pop("auto_adjust_settings"),
|
|
571
|
-
**kwargs,
|
|
572
|
-
)
|
|
573
|
-
else:
|
|
574
|
-
return self._text_point(
|
|
575
|
-
ra,
|
|
576
|
-
dec,
|
|
577
|
-
text,
|
|
578
|
-
**style.matplot_kwargs(self.scale),
|
|
579
|
-
hide_on_collision=hide_on_collision,
|
|
580
|
-
xycoords="data",
|
|
581
|
-
xytext=(style.offset_x * self.scale, style.offset_y * self.scale),
|
|
582
|
-
textcoords="offset points",
|
|
583
|
-
**kwargs,
|
|
584
|
-
)
|
|
585
|
-
|
|
586
171
|
@property
|
|
587
172
|
def objects(self) -> models.ObjectList:
|
|
588
173
|
"""
|
|
@@ -637,6 +222,7 @@ class BasePlot(ABC):
|
|
|
637
222
|
label: Optional[str] = None,
|
|
638
223
|
legend_label: str = None,
|
|
639
224
|
skip_bounds_check: bool = False,
|
|
225
|
+
collision_handler: CollisionHandler = None,
|
|
640
226
|
**kwargs,
|
|
641
227
|
) -> None:
|
|
642
228
|
"""Plots a marker
|
|
@@ -648,6 +234,7 @@ class BasePlot(ABC):
|
|
|
648
234
|
style: Styling for the marker
|
|
649
235
|
legend_label: How to label the marker in the legend. If `None`, then the marker will not be added to the legend
|
|
650
236
|
skip_bounds_check: If True, then don't check the marker coordinates to ensure they're within the bounds of the plot. If you're plotting many markers, setting this to True can speed up plotting time.
|
|
237
|
+
collision_handler: An instance of [CollisionHandler][starplot.CollisionHandler] that describes what to do on label collisions with other labels, markers, etc. If `None`, then the collision handler of the plot will be used.
|
|
651
238
|
|
|
652
239
|
"""
|
|
653
240
|
|
|
@@ -700,7 +287,7 @@ class BasePlot(ABC):
|
|
|
700
287
|
ra,
|
|
701
288
|
dec,
|
|
702
289
|
label_style,
|
|
703
|
-
|
|
290
|
+
collision_handler=collision_handler or self.collision_handler,
|
|
704
291
|
gid=kwargs.get("gid_label") or "marker-label",
|
|
705
292
|
)
|
|
706
293
|
|
|
@@ -714,6 +301,7 @@ class BasePlot(ABC):
|
|
|
714
301
|
true_size: bool = False,
|
|
715
302
|
labels: Dict[PlanetName, str] = PLANET_LABELS_DEFAULT,
|
|
716
303
|
legend_label: str = "Planet",
|
|
304
|
+
collision_handler: CollisionHandler = None,
|
|
717
305
|
) -> None:
|
|
718
306
|
"""
|
|
719
307
|
Plots the planets.
|
|
@@ -725,6 +313,7 @@ class BasePlot(ABC):
|
|
|
725
313
|
true_size: If True, then each planet's true apparent size in the sky will be plotted. If False, then the style's marker size will be used.
|
|
726
314
|
labels: How the planets will be labeled on the plot and legend. If not specified, then the planet's name will be used (see [`Planet`][starplot.models.planet.PlanetName])
|
|
727
315
|
legend_label: How to label the planets in the legend. If `None`, then the planets will not be added to the legend
|
|
316
|
+
collision_handler: An instance of [CollisionHandler][starplot.CollisionHandler] that describes what to do on label collisions with other labels, markers, etc. If `None`, then the collision handler of the plot will be used.
|
|
728
317
|
"""
|
|
729
318
|
labels = labels or {}
|
|
730
319
|
planets = models.Planet.all(
|
|
@@ -732,6 +321,7 @@ class BasePlot(ABC):
|
|
|
732
321
|
)
|
|
733
322
|
|
|
734
323
|
legend_label = translate(legend_label, self.language)
|
|
324
|
+
handler = collision_handler or self.collision_handler
|
|
735
325
|
|
|
736
326
|
for p in planets:
|
|
737
327
|
label = labels.get(p.name)
|
|
@@ -744,16 +334,21 @@ class BasePlot(ABC):
|
|
|
744
334
|
polygon_style = style.marker.to_polygon_style()
|
|
745
335
|
polygon_style.edge_color = None
|
|
746
336
|
self.circle(
|
|
747
|
-
(p.ra, p.dec),
|
|
748
|
-
p.apparent_size,
|
|
749
|
-
polygon_style,
|
|
337
|
+
center=(p.ra, p.dec),
|
|
338
|
+
radius_degrees=p.apparent_size / 2,
|
|
339
|
+
style=polygon_style,
|
|
750
340
|
gid="planet-marker",
|
|
751
341
|
)
|
|
752
342
|
self._add_legend_handle_marker(legend_label, style.marker)
|
|
753
343
|
|
|
754
344
|
if label:
|
|
755
345
|
self.text(
|
|
756
|
-
label.upper(),
|
|
346
|
+
label.upper(),
|
|
347
|
+
p.ra,
|
|
348
|
+
p.dec,
|
|
349
|
+
style.label,
|
|
350
|
+
collision_handler=handler,
|
|
351
|
+
gid="planet-label",
|
|
757
352
|
)
|
|
758
353
|
else:
|
|
759
354
|
self.marker(
|
|
@@ -762,6 +357,7 @@ class BasePlot(ABC):
|
|
|
762
357
|
style=style,
|
|
763
358
|
label=label.upper() if label else None,
|
|
764
359
|
legend_label=legend_label,
|
|
360
|
+
collision_handler=handler,
|
|
765
361
|
gid_marker="planet-marker",
|
|
766
362
|
gid_label="planet-label",
|
|
767
363
|
)
|
|
@@ -773,6 +369,7 @@ class BasePlot(ABC):
|
|
|
773
369
|
true_size: bool = False,
|
|
774
370
|
label: str = "Sun",
|
|
775
371
|
legend_label: str = "Sun",
|
|
372
|
+
collision_handler: CollisionHandler = None,
|
|
776
373
|
) -> None:
|
|
777
374
|
"""
|
|
778
375
|
Plots the Sun.
|
|
@@ -784,6 +381,7 @@ class BasePlot(ABC):
|
|
|
784
381
|
true_size: If True, then the Sun's true apparent size in the sky will be plotted as a circle (the marker style's symbol will be ignored). If False, then the style's marker size will be used.
|
|
785
382
|
label: How the Sun will be labeled on the plot
|
|
786
383
|
legend_label: How the sun will be labeled in the legend
|
|
384
|
+
collision_handler: An instance of [CollisionHandler][starplot.CollisionHandler] that describes what to do on label collisions with other labels, markers, etc. If `None`, then the collision handler of the plot will be used.
|
|
787
385
|
"""
|
|
788
386
|
s = models.Sun.get(
|
|
789
387
|
dt=self.observer.dt,
|
|
@@ -794,6 +392,7 @@ class BasePlot(ABC):
|
|
|
794
392
|
label = translate(label, self.language)
|
|
795
393
|
legend_label = translate(legend_label, self.language)
|
|
796
394
|
s.name = label or s.name
|
|
395
|
+
handler = collision_handler or self.collision_handler
|
|
797
396
|
|
|
798
397
|
if not self.in_bounds(s.ra, s.dec):
|
|
799
398
|
return
|
|
@@ -807,17 +406,25 @@ class BasePlot(ABC):
|
|
|
807
406
|
polygon_style.edge_color = None
|
|
808
407
|
|
|
809
408
|
self.circle(
|
|
810
|
-
(s.ra, s.dec),
|
|
811
|
-
s.apparent_size,
|
|
409
|
+
center=(s.ra, s.dec),
|
|
410
|
+
radius_degrees=s.apparent_size / 2,
|
|
812
411
|
style=polygon_style,
|
|
813
412
|
gid="sun-marker",
|
|
413
|
+
num_pts=200,
|
|
814
414
|
)
|
|
815
415
|
|
|
816
416
|
style.marker.symbol = MarkerSymbolEnum.CIRCLE
|
|
817
417
|
self._add_legend_handle_marker(legend_label, style.marker)
|
|
818
418
|
|
|
819
419
|
if label:
|
|
820
|
-
self.text(
|
|
420
|
+
self.text(
|
|
421
|
+
label,
|
|
422
|
+
s.ra,
|
|
423
|
+
s.dec,
|
|
424
|
+
style.label,
|
|
425
|
+
collision_handler=handler,
|
|
426
|
+
gid="sun-label",
|
|
427
|
+
)
|
|
821
428
|
|
|
822
429
|
else:
|
|
823
430
|
self.marker(
|
|
@@ -826,6 +433,7 @@ class BasePlot(ABC):
|
|
|
826
433
|
style=style,
|
|
827
434
|
label=label,
|
|
828
435
|
legend_label=legend_label,
|
|
436
|
+
collision_handler=handler,
|
|
829
437
|
gid_marker="sun-marker",
|
|
830
438
|
gid_label="sun-label",
|
|
831
439
|
)
|
|
@@ -1026,14 +634,27 @@ class BasePlot(ABC):
|
|
|
1026
634
|
)
|
|
1027
635
|
|
|
1028
636
|
@use_style(LineStyle)
|
|
1029
|
-
def line(
|
|
637
|
+
def line(
|
|
638
|
+
self,
|
|
639
|
+
style: LineStyle,
|
|
640
|
+
coordinates: list[tuple[float, float]] = None,
|
|
641
|
+
geometry: LineString = None,
|
|
642
|
+
**kwargs,
|
|
643
|
+
):
|
|
1030
644
|
"""Plots a line
|
|
1031
645
|
|
|
1032
646
|
Args:
|
|
1033
647
|
coordinates: List of coordinates, e.g. `[(ra, dec), (ra, dec)]`
|
|
648
|
+
geometry: A shapely LineString. If this value is passed, then the `coordinates` kwarg will be ignored.
|
|
1034
649
|
style: Style of the line
|
|
1035
650
|
"""
|
|
1036
|
-
|
|
651
|
+
|
|
652
|
+
if coordinates is None and geometry is None:
|
|
653
|
+
raise ValueError("Must pass coordinates or geometry when plotting lines.")
|
|
654
|
+
|
|
655
|
+
coords = geometry.coords if geometry is not None else coordinates
|
|
656
|
+
|
|
657
|
+
x, y = zip(*[self._prepare_coords(*p) for p in coords])
|
|
1037
658
|
|
|
1038
659
|
self.ax.plot(
|
|
1039
660
|
x,
|
|
@@ -1053,6 +674,7 @@ class BasePlot(ABC):
|
|
|
1053
674
|
show_phase: bool = False,
|
|
1054
675
|
label: str = "Moon",
|
|
1055
676
|
legend_label: str = "Moon",
|
|
677
|
+
collision_handler: CollisionHandler = None,
|
|
1056
678
|
) -> None:
|
|
1057
679
|
"""
|
|
1058
680
|
Plots the Moon.
|
|
@@ -1063,7 +685,9 @@ class BasePlot(ABC):
|
|
|
1063
685
|
style: Styling of the Moon. If None, then the plot's style (specified when creating the plot) will be used
|
|
1064
686
|
true_size: If True, then the Moon's true apparent size in the sky will be plotted as a circle (the marker style's symbol will be ignored). If False, then the style's marker size will be used.
|
|
1065
687
|
show_phase: If True, and if `true_size = True`, then the approximate phase of the moon will be illustrated. The dark side of the moon will be colored with the marker's `edge_color`.
|
|
1066
|
-
label: How the Moon will be labeled on the plot
|
|
688
|
+
label: How the Moon will be labeled on the plot
|
|
689
|
+
legend_label: How the Moon will be labeled in the legend
|
|
690
|
+
collision_handler: An instance of [CollisionHandler][starplot.CollisionHandler] that describes what to do on label collisions with other labels, markers, etc. If `None`, then the collision handler of the plot will be used.
|
|
1067
691
|
"""
|
|
1068
692
|
m = models.Moon.get(
|
|
1069
693
|
dt=self.observer.dt,
|
|
@@ -1074,6 +698,7 @@ class BasePlot(ABC):
|
|
|
1074
698
|
label = translate(label, self.language)
|
|
1075
699
|
legend_label = translate(legend_label, self.language)
|
|
1076
700
|
m.name = label or m.name
|
|
701
|
+
handler = collision_handler or self.collision_handler
|
|
1077
702
|
|
|
1078
703
|
if not self.in_bounds(m.ra, m.dec):
|
|
1079
704
|
return
|
|
@@ -1089,9 +714,9 @@ class BasePlot(ABC):
|
|
|
1089
714
|
|
|
1090
715
|
if show_phase:
|
|
1091
716
|
self._moon_with_phase(
|
|
1092
|
-
m.phase_description,
|
|
1093
|
-
(m.ra, m.dec),
|
|
1094
|
-
m.apparent_size,
|
|
717
|
+
moon_phase=m.phase_description,
|
|
718
|
+
center=(m.ra, m.dec),
|
|
719
|
+
radius_degrees=m.apparent_size / 2,
|
|
1095
720
|
style=polygon_style,
|
|
1096
721
|
dark_side_color=style.marker.edge_color,
|
|
1097
722
|
)
|
|
@@ -1107,7 +732,14 @@ class BasePlot(ABC):
|
|
|
1107
732
|
self._add_legend_handle_marker(legend_label, style.marker)
|
|
1108
733
|
|
|
1109
734
|
if label:
|
|
1110
|
-
self.text(
|
|
735
|
+
self.text(
|
|
736
|
+
label,
|
|
737
|
+
m.ra,
|
|
738
|
+
m.dec,
|
|
739
|
+
style.label,
|
|
740
|
+
collision_handler=handler,
|
|
741
|
+
gid="moon-label",
|
|
742
|
+
)
|
|
1111
743
|
|
|
1112
744
|
else:
|
|
1113
745
|
self.marker(
|
|
@@ -1116,6 +748,7 @@ class BasePlot(ABC):
|
|
|
1116
748
|
style=style,
|
|
1117
749
|
label=label,
|
|
1118
750
|
legend_label=legend_label,
|
|
751
|
+
collision_handler=handler,
|
|
1119
752
|
gid_marker="moon-marker",
|
|
1120
753
|
gid_label="moon-label",
|
|
1121
754
|
)
|
|
@@ -1178,8 +811,8 @@ class BasePlot(ABC):
|
|
|
1178
811
|
# Plot left side
|
|
1179
812
|
self.ellipse(
|
|
1180
813
|
center,
|
|
1181
|
-
radius_degrees * 2,
|
|
1182
|
-
radius_degrees * 2,
|
|
814
|
+
height_degrees=radius_degrees * 2,
|
|
815
|
+
width_degrees=radius_degrees * 2,
|
|
1183
816
|
style=left,
|
|
1184
817
|
num_pts=num_pts,
|
|
1185
818
|
angle=0,
|
|
@@ -1189,8 +822,8 @@ class BasePlot(ABC):
|
|
|
1189
822
|
# Plot right side
|
|
1190
823
|
self.ellipse(
|
|
1191
824
|
center,
|
|
1192
|
-
radius_degrees * 2,
|
|
1193
|
-
radius_degrees * 2,
|
|
825
|
+
height_degrees=radius_degrees * 2,
|
|
826
|
+
width_degrees=radius_degrees * 2,
|
|
1194
827
|
style=right,
|
|
1195
828
|
num_pts=num_pts,
|
|
1196
829
|
angle=180,
|
|
@@ -1200,8 +833,8 @@ class BasePlot(ABC):
|
|
|
1200
833
|
# Plot middle
|
|
1201
834
|
self.ellipse(
|
|
1202
835
|
center,
|
|
1203
|
-
radius_degrees * 2,
|
|
1204
|
-
radius_degrees,
|
|
836
|
+
height_degrees=radius_degrees * 2,
|
|
837
|
+
width_degrees=radius_degrees,
|
|
1205
838
|
style=middle,
|
|
1206
839
|
gid="moon-marker",
|
|
1207
840
|
)
|
|
@@ -1238,12 +871,18 @@ class BasePlot(ABC):
|
|
|
1238
871
|
)
|
|
1239
872
|
|
|
1240
873
|
@use_style(PathStyle, "ecliptic")
|
|
1241
|
-
def ecliptic(
|
|
874
|
+
def ecliptic(
|
|
875
|
+
self,
|
|
876
|
+
style: PathStyle = None,
|
|
877
|
+
label: str = "ECLIPTIC",
|
|
878
|
+
collision_handler: CollisionHandler = None,
|
|
879
|
+
):
|
|
1242
880
|
"""Plots the ecliptic
|
|
1243
881
|
|
|
1244
882
|
Args:
|
|
1245
883
|
style: Styling of the ecliptic. If None, then the plot's style will be used
|
|
1246
884
|
label: How the ecliptic will be labeled on the plot
|
|
885
|
+
collision_handler: An instance of [CollisionHandler][starplot.CollisionHandler] that describes what to do on label collisions with other labels, markers, etc. If `None`, then the collision handler of the plot will be used.
|
|
1247
886
|
"""
|
|
1248
887
|
x = []
|
|
1249
888
|
y = []
|
|
@@ -1272,11 +911,21 @@ class BasePlot(ABC):
|
|
|
1272
911
|
label_spacing = int(len(inbounds) / 4)
|
|
1273
912
|
|
|
1274
913
|
for ra, dec in [inbounds[label_spacing], inbounds[label_spacing * 2]]:
|
|
1275
|
-
self.text(
|
|
914
|
+
self.text(
|
|
915
|
+
label,
|
|
916
|
+
ra,
|
|
917
|
+
dec,
|
|
918
|
+
style.label,
|
|
919
|
+
collision_handler=collision_handler or self.collision_handler,
|
|
920
|
+
gid="ecliptic-label",
|
|
921
|
+
)
|
|
1276
922
|
|
|
1277
923
|
@use_style(PathStyle, "celestial_equator")
|
|
1278
924
|
def celestial_equator(
|
|
1279
|
-
self,
|
|
925
|
+
self,
|
|
926
|
+
style: PathStyle = None,
|
|
927
|
+
label: str = "CELESTIAL EQUATOR",
|
|
928
|
+
collision_handler: CollisionHandler = None,
|
|
1280
929
|
):
|
|
1281
930
|
"""
|
|
1282
931
|
Plots the celestial equator
|
|
@@ -1284,6 +933,7 @@ class BasePlot(ABC):
|
|
|
1284
933
|
Args:
|
|
1285
934
|
style: Styling of the celestial equator. If None, then the plot's style will be used
|
|
1286
935
|
label: How the celestial equator will be labeled on the plot
|
|
936
|
+
collision_handler: An instance of [CollisionHandler][starplot.CollisionHandler] that describes what to do on label collisions with other labels, markers, etc. If `None`, then the collision handler of the plot will be used.
|
|
1287
937
|
"""
|
|
1288
938
|
x = []
|
|
1289
939
|
y = []
|
|
@@ -1314,5 +964,6 @@ class BasePlot(ABC):
|
|
|
1314
964
|
ra,
|
|
1315
965
|
0.25,
|
|
1316
966
|
style.label,
|
|
967
|
+
collision_handler=collision_handler or self.collision_handler,
|
|
1317
968
|
gid="celestial-equator-label",
|
|
1318
969
|
)
|