starplot 0.13.0__py2.py3-none-any.whl → 0.15.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- starplot/__init__.py +5 -2
- starplot/base.py +311 -197
- starplot/cli.py +33 -0
- starplot/coordinates.py +6 -0
- starplot/data/__init__.py +6 -24
- starplot/data/bigsky.py +58 -40
- starplot/data/constellation_lines.py +827 -0
- starplot/data/constellation_stars.py +1501 -0
- starplot/data/constellations.py +600 -27
- starplot/data/db.py +17 -0
- starplot/data/dsos.py +24 -141
- starplot/data/stars.py +45 -24
- starplot/geod.py +0 -6
- starplot/geometry.py +181 -0
- starplot/horizon.py +302 -231
- starplot/map.py +100 -463
- starplot/mixins.py +75 -14
- starplot/models/__init__.py +1 -1
- starplot/models/base.py +18 -129
- starplot/models/constellation.py +55 -32
- starplot/models/dso.py +132 -67
- starplot/models/moon.py +57 -78
- starplot/models/planet.py +44 -69
- starplot/models/star.py +91 -60
- starplot/models/sun.py +32 -53
- starplot/optic.py +21 -18
- starplot/plotters/__init__.py +2 -0
- starplot/plotters/constellations.py +342 -0
- starplot/plotters/dsos.py +49 -68
- starplot/plotters/experimental.py +171 -0
- starplot/plotters/milkyway.py +39 -0
- starplot/plotters/stars.py +126 -122
- starplot/profile.py +16 -0
- starplot/settings.py +26 -0
- starplot/styles/__init__.py +2 -0
- starplot/styles/base.py +56 -34
- starplot/styles/ext/antique.yml +11 -9
- starplot/styles/ext/blue_dark.yml +8 -10
- starplot/styles/ext/blue_gold.yml +135 -0
- starplot/styles/ext/blue_light.yml +14 -12
- starplot/styles/ext/blue_medium.yml +23 -20
- 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/styles/extensions.py +1 -0
- starplot/utils.py +19 -0
- starplot/warnings.py +21 -0
- {starplot-0.13.0.dist-info → starplot-0.15.0.dist-info}/METADATA +19 -18
- starplot-0.15.0.dist-info/RECORD +97 -0
- starplot-0.15.0.dist-info/entry_points.txt +3 -0
- starplot/data/bayer.py +0 -3499
- starplot/data/flamsteed.py +0 -2682
- starplot/data/library/constellation_borders_inv.gpkg +0 -0
- starplot/data/library/constellation_lines_hips.json +0 -709
- starplot/data/library/constellation_lines_inv.gpkg +0 -0
- starplot/data/library/constellations.gpkg +0 -0
- starplot/data/library/constellations_hip.fab +0 -88
- starplot/data/library/milkyway.gpkg +0 -0
- starplot/data/library/milkyway_inv.gpkg +0 -0
- starplot/data/library/ongc.gpkg.zip +0 -0
- starplot/data/library/stars.bigsky.mag11.parquet +0 -0
- starplot/data/library/stars.hipparcos.parquet +0 -0
- starplot/data/messier.py +0 -111
- starplot/data/prep/__init__.py +0 -0
- starplot/data/prep/constellations.py +0 -108
- starplot/data/prep/dsos.py +0 -299
- starplot/data/prep/utils.py +0 -16
- starplot/models/geometry.py +0 -44
- starplot-0.13.0.dist-info/RECORD +0 -101
- {starplot-0.13.0.dist-info → starplot-0.15.0.dist-info}/LICENSE +0 -0
- {starplot-0.13.0.dist-info → starplot-0.15.0.dist-info}/WHEEL +0 -0
starplot/base.py
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
2
|
from datetime import datetime
|
|
3
3
|
from typing import Dict, Union, Optional
|
|
4
|
+
from random import randrange
|
|
4
5
|
import logging
|
|
5
|
-
import warnings
|
|
6
6
|
|
|
7
7
|
import numpy as np
|
|
8
8
|
import rtree
|
|
9
|
-
from adjustText import adjust_text as _adjust_text
|
|
10
9
|
from matplotlib import patches
|
|
11
10
|
from matplotlib import pyplot as plt, patheffects
|
|
12
11
|
from matplotlib.lines import Line2D
|
|
13
12
|
from pytz import timezone
|
|
14
|
-
from shapely import Polygon
|
|
13
|
+
from shapely import Polygon, Point
|
|
15
14
|
|
|
16
|
-
from starplot import
|
|
15
|
+
from starplot.coordinates import CoordinateSystem
|
|
16
|
+
from starplot import geod, models, warnings
|
|
17
17
|
from starplot.data import load, ecliptic
|
|
18
18
|
from starplot.models.planet import PlanetName, PLANET_LABELS_DEFAULT
|
|
19
19
|
from starplot.models.moon import MoonPhase
|
|
@@ -32,12 +32,11 @@ from starplot.styles import (
|
|
|
32
32
|
fonts,
|
|
33
33
|
)
|
|
34
34
|
from starplot.styles.helpers import use_style
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"ignore",
|
|
39
|
-
message="Setting the 'color' property will override the edgecolor or facecolor properties",
|
|
35
|
+
from starplot.geometry import (
|
|
36
|
+
unwrap_polygon_360,
|
|
37
|
+
random_point_in_polygon_at_distance,
|
|
40
38
|
)
|
|
39
|
+
from starplot.profile import profile
|
|
41
40
|
|
|
42
41
|
LOGGER = logging.getLogger("starplot")
|
|
43
42
|
LOG_HANDLER = logging.StreamHandler()
|
|
@@ -49,7 +48,11 @@ LOGGER.addHandler(LOG_HANDLER)
|
|
|
49
48
|
|
|
50
49
|
|
|
51
50
|
DEFAULT_FOV_STYLE = PolygonStyle(
|
|
52
|
-
fill_color=None,
|
|
51
|
+
fill_color=None,
|
|
52
|
+
edge_color="red",
|
|
53
|
+
line_style=[1, [2, 3]],
|
|
54
|
+
edge_width=3,
|
|
55
|
+
zorder=-1000,
|
|
53
56
|
)
|
|
54
57
|
"""Default style for plotting scope and bino field of view circles"""
|
|
55
58
|
|
|
@@ -62,6 +65,8 @@ DPI = 100
|
|
|
62
65
|
|
|
63
66
|
class BasePlot(ABC):
|
|
64
67
|
_background_clip_path = None
|
|
68
|
+
_clip_path_polygon: Polygon = None # clip path in display coordinates
|
|
69
|
+
_coordinate_system = CoordinateSystem.RA_DEC
|
|
65
70
|
|
|
66
71
|
def __init__(
|
|
67
72
|
self,
|
|
@@ -72,11 +77,11 @@ class BasePlot(ABC):
|
|
|
72
77
|
hide_colliding_labels: bool = True,
|
|
73
78
|
scale: float = 1.0,
|
|
74
79
|
autoscale: bool = False,
|
|
80
|
+
suppress_warnings: bool = True,
|
|
75
81
|
*args,
|
|
76
82
|
**kwargs,
|
|
77
83
|
):
|
|
78
84
|
px = 1 / DPI # plt.rcParams["figure.dpi"] # pixel in inches
|
|
79
|
-
|
|
80
85
|
self.pixels_per_point = DPI / 72
|
|
81
86
|
|
|
82
87
|
self.style = style
|
|
@@ -89,16 +94,19 @@ class BasePlot(ABC):
|
|
|
89
94
|
if self.autoscale:
|
|
90
95
|
self.scale = self.resolution / DEFAULT_RESOLUTION
|
|
91
96
|
|
|
97
|
+
if suppress_warnings:
|
|
98
|
+
warnings.suppress()
|
|
99
|
+
|
|
92
100
|
self.dt = dt or timezone("UTC").localize(datetime.now())
|
|
93
101
|
self._ephemeris_name = ephemeris
|
|
94
102
|
self.ephemeris = load(ephemeris)
|
|
103
|
+
self.earth = self.ephemeris["earth"]
|
|
95
104
|
|
|
96
105
|
self.labels = []
|
|
97
106
|
self._labels_rtree = rtree.index.Index()
|
|
98
|
-
|
|
99
|
-
# self.labels = []
|
|
100
107
|
self._constellations_rtree = rtree.index.Index()
|
|
101
108
|
self._stars_rtree = rtree.index.Index()
|
|
109
|
+
self._markers_rtree = rtree.index.Index()
|
|
102
110
|
|
|
103
111
|
self._background_clip_path = None
|
|
104
112
|
|
|
@@ -125,32 +133,34 @@ class BasePlot(ABC):
|
|
|
125
133
|
def _prepare_coords(self, ra, dec) -> tuple[float, float]:
|
|
126
134
|
return ra, dec
|
|
127
135
|
|
|
128
|
-
def
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
)
|
|
136
|
+
def _update_clip_path_polygon(self, buffer=8):
|
|
137
|
+
coords = self._background_clip_path.get_verts()
|
|
138
|
+
self._clip_path_polygon = Polygon(coords).buffer(-1 * buffer)
|
|
139
|
+
|
|
140
|
+
def _is_label_collision(self, bbox) -> bool:
|
|
141
|
+
ix = list(self._labels_rtree.intersection(bbox))
|
|
134
142
|
return len(ix) > 0
|
|
135
143
|
|
|
136
|
-
def
|
|
137
|
-
ix = list(
|
|
138
|
-
self._constellations_rtree.intersection(
|
|
139
|
-
(extent.x0, extent.y0, extent.x1, extent.y1)
|
|
140
|
-
)
|
|
141
|
-
)
|
|
144
|
+
def _is_constellation_collision(self, bbox) -> bool:
|
|
145
|
+
ix = list(self._constellations_rtree.intersection(bbox))
|
|
142
146
|
return len(ix) > 0
|
|
143
147
|
|
|
144
|
-
def _is_star_collision(self,
|
|
145
|
-
ix = list(
|
|
146
|
-
self._stars_rtree.intersection((extent.x0, extent.y0, extent.x1, extent.y1))
|
|
147
|
-
)
|
|
148
|
+
def _is_star_collision(self, bbox) -> bool:
|
|
149
|
+
ix = list(self._stars_rtree.intersection(bbox))
|
|
148
150
|
return len(ix) > 0
|
|
149
151
|
|
|
150
|
-
def
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
def _is_marker_collision(self, bbox) -> bool:
|
|
153
|
+
ix = list(self._markers_rtree.intersection(bbox))
|
|
154
|
+
return len(ix) > 0
|
|
155
|
+
|
|
156
|
+
def _is_clipped(self, points) -> bool:
|
|
157
|
+
p = self._clip_path_polygon
|
|
158
|
+
|
|
159
|
+
for x, y in points:
|
|
160
|
+
if not p.contains(Point(x, y)):
|
|
161
|
+
return True
|
|
162
|
+
|
|
163
|
+
return False
|
|
154
164
|
|
|
155
165
|
def _add_label_to_rtree(self, label, extent=None):
|
|
156
166
|
extent = extent or label.get_window_extent(
|
|
@@ -158,31 +168,94 @@ class BasePlot(ABC):
|
|
|
158
168
|
)
|
|
159
169
|
self.labels.append(label)
|
|
160
170
|
self._labels_rtree.insert(
|
|
161
|
-
0, np.array((extent.x0, extent.y0, extent.x1, extent.y1))
|
|
171
|
+
0, np.array((extent.x0 - 1, extent.y0 - 1, extent.x1 + 1, extent.y1 + 1))
|
|
162
172
|
)
|
|
163
173
|
|
|
174
|
+
def _is_open_space(
|
|
175
|
+
self,
|
|
176
|
+
bbox: tuple[float, float, float, float],
|
|
177
|
+
padding=0,
|
|
178
|
+
avoid_clipped=True,
|
|
179
|
+
avoid_label_collisions=True,
|
|
180
|
+
avoid_marker_collisions=True,
|
|
181
|
+
avoid_constellation_collision=True,
|
|
182
|
+
) -> bool:
|
|
183
|
+
"""
|
|
184
|
+
Returns true if the boox covers an open space (i.e. no collisions)
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
bbox: 4-element tuple of lower left and upper right coordinates
|
|
188
|
+
"""
|
|
189
|
+
x0, y0, x1, y1 = bbox
|
|
190
|
+
points = [(x0, y0), (x1, y1)]
|
|
191
|
+
bbox = (
|
|
192
|
+
x0 - padding,
|
|
193
|
+
y0 - padding,
|
|
194
|
+
x1 + padding,
|
|
195
|
+
y1 + padding,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if any([np.isnan(c) for c in (x0, y0, x1, y1)]):
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
if avoid_clipped and self._is_clipped(points):
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
if avoid_label_collisions and self._is_label_collision(bbox):
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
if avoid_marker_collisions and (
|
|
208
|
+
self._is_star_collision(bbox) or self._is_marker_collision(bbox)
|
|
209
|
+
):
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
if avoid_constellation_collision and self._is_constellation_collision(bbox):
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
return True
|
|
216
|
+
|
|
217
|
+
def _get_label_bbox(self, label):
|
|
218
|
+
extent = label.get_window_extent(renderer=self.fig.canvas.get_renderer())
|
|
219
|
+
return (extent.x0, extent.y0, extent.x1, extent.y1)
|
|
220
|
+
|
|
164
221
|
def _maybe_remove_label(
|
|
165
|
-
self,
|
|
222
|
+
self,
|
|
223
|
+
label,
|
|
224
|
+
remove_on_collision=True,
|
|
225
|
+
remove_on_clipped=True,
|
|
226
|
+
remove_on_constellation_collision=True,
|
|
227
|
+
padding=0,
|
|
166
228
|
) -> bool:
|
|
167
229
|
"""Returns true if the label is removed, else false"""
|
|
168
230
|
extent = label.get_window_extent(renderer=self.fig.canvas.get_renderer())
|
|
231
|
+
bbox = (
|
|
232
|
+
extent.x0 - padding,
|
|
233
|
+
extent.y0 - padding,
|
|
234
|
+
extent.x1 + padding,
|
|
235
|
+
extent.y1 + padding,
|
|
236
|
+
)
|
|
237
|
+
points = [(extent.x0, extent.y0), (extent.x1, extent.y1)]
|
|
169
238
|
|
|
170
239
|
if any([np.isnan(c) for c in (extent.x0, extent.y0, extent.x1, extent.y1)]):
|
|
171
240
|
label.remove()
|
|
172
241
|
return True
|
|
173
242
|
|
|
174
|
-
if remove_on_clipped and self._is_clipped(
|
|
243
|
+
if remove_on_clipped and self._is_clipped(points):
|
|
175
244
|
label.remove()
|
|
176
245
|
return True
|
|
177
246
|
|
|
178
247
|
if remove_on_collision and (
|
|
179
|
-
self._is_label_collision(
|
|
180
|
-
or self.
|
|
181
|
-
|
|
248
|
+
self._is_label_collision(bbox)
|
|
249
|
+
or self._is_star_collision(bbox)
|
|
250
|
+
or self._is_marker_collision(bbox)
|
|
182
251
|
):
|
|
183
252
|
label.remove()
|
|
184
253
|
return True
|
|
185
254
|
|
|
255
|
+
if remove_on_constellation_collision and self._is_constellation_collision(bbox):
|
|
256
|
+
label.remove()
|
|
257
|
+
return True
|
|
258
|
+
|
|
186
259
|
return False
|
|
187
260
|
|
|
188
261
|
def _add_legend_handle_marker(self, label: str, style: MarkerStyle):
|
|
@@ -260,70 +333,44 @@ class BasePlot(ABC):
|
|
|
260
333
|
|
|
261
334
|
return sum([x_labels, x_constellations, x_stars]) / 3
|
|
262
335
|
|
|
263
|
-
def
|
|
336
|
+
def _text(self, x, y, text, **kwargs):
|
|
337
|
+
label = self.ax.annotate(
|
|
338
|
+
text,
|
|
339
|
+
(x, y),
|
|
340
|
+
**kwargs,
|
|
341
|
+
**self._plot_kwargs(),
|
|
342
|
+
)
|
|
343
|
+
if kwargs.get("clip_on"):
|
|
344
|
+
label.set_clip_on(True)
|
|
345
|
+
label.set_clip_path(self._background_clip_path)
|
|
346
|
+
return label
|
|
347
|
+
|
|
348
|
+
def _text_point(
|
|
264
349
|
self,
|
|
265
350
|
ra: float,
|
|
266
351
|
dec: float,
|
|
267
352
|
text: str,
|
|
268
353
|
hide_on_collision: bool = True,
|
|
269
|
-
|
|
270
|
-
|
|
354
|
+
force: bool = False,
|
|
355
|
+
clip_on: bool = True,
|
|
271
356
|
**kwargs,
|
|
272
|
-
)
|
|
357
|
+
):
|
|
273
358
|
if not text:
|
|
274
|
-
return
|
|
359
|
+
return None
|
|
275
360
|
|
|
276
361
|
x, y = self._prepare_coords(ra, dec)
|
|
277
362
|
kwargs["path_effects"] = kwargs.get("path_effects", [self.text_border])
|
|
278
|
-
clip_on = kwargs.get("clip_on") or True
|
|
279
363
|
|
|
280
|
-
def plot_text(**kwargs):
|
|
281
|
-
label = self.ax.annotate(
|
|
282
|
-
text,
|
|
283
|
-
(x, y),
|
|
284
|
-
*args,
|
|
285
|
-
**kwargs,
|
|
286
|
-
**self._plot_kwargs(),
|
|
287
|
-
)
|
|
288
|
-
if clip_on:
|
|
289
|
-
label.set_clip_on(True)
|
|
290
|
-
label.set_clip_path(self._background_clip_path)
|
|
291
|
-
return label
|
|
292
|
-
|
|
293
|
-
def add_label(label):
|
|
294
|
-
extent = label.get_window_extent(renderer=self.fig.canvas.get_renderer())
|
|
295
|
-
self.labels.append(label)
|
|
296
|
-
self._labels_rtree.insert(
|
|
297
|
-
0, np.array((extent.x0, extent.y0, extent.x1, extent.y1))
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
label = plot_text(**kwargs)
|
|
301
|
-
|
|
302
|
-
if not clip_on:
|
|
303
|
-
add_label(label)
|
|
304
|
-
return
|
|
305
|
-
|
|
306
|
-
if not hide_on_collision and not auto_anchor:
|
|
307
|
-
add_label(label)
|
|
308
|
-
return
|
|
309
|
-
|
|
310
|
-
# removed = self._maybe_remove_label(label)
|
|
311
|
-
collision = self._collision_score(label)
|
|
312
|
-
|
|
313
|
-
if collision == 0:
|
|
314
|
-
add_label(label)
|
|
315
|
-
return
|
|
316
|
-
|
|
317
|
-
label.remove()
|
|
318
|
-
|
|
319
|
-
collision_scores = []
|
|
320
364
|
original_va = kwargs.pop("va", None)
|
|
321
365
|
original_ha = kwargs.pop("ha", None)
|
|
322
366
|
original_offset_x, original_offset_y = kwargs.pop("xytext", (0, 0))
|
|
323
|
-
|
|
324
|
-
|
|
367
|
+
|
|
368
|
+
anchors = [(original_va, original_ha)]
|
|
369
|
+
for a in self.style.text_anchor_fallbacks:
|
|
325
370
|
d = AnchorPointEnum.from_str(a).as_matplot()
|
|
326
|
-
|
|
371
|
+
anchors.append((d["va"], d["ha"]))
|
|
372
|
+
|
|
373
|
+
for va, ha in anchors:
|
|
327
374
|
offset_x, offset_y = original_offset_x, original_offset_y
|
|
328
375
|
if original_ha != ha:
|
|
329
376
|
offset_x *= -1
|
|
@@ -335,100 +382,112 @@ class BasePlot(ABC):
|
|
|
335
382
|
offset_x = 0
|
|
336
383
|
offset_y = 0
|
|
337
384
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
collision = self._collision_score(label)
|
|
345
|
-
if collision == 0:
|
|
346
|
-
add_label(label)
|
|
347
|
-
return
|
|
385
|
+
label = self._text(
|
|
386
|
+
x, y, text, **kwargs, va=va, ha=ha, xytext=(offset_x, offset_y)
|
|
387
|
+
)
|
|
388
|
+
removed = self._maybe_remove_label(
|
|
389
|
+
label, remove_on_collision=hide_on_collision, remove_on_clipped=clip_on
|
|
390
|
+
)
|
|
348
391
|
|
|
349
|
-
if
|
|
350
|
-
|
|
392
|
+
if force or not removed:
|
|
393
|
+
self._add_label_to_rtree(label)
|
|
394
|
+
return label
|
|
351
395
|
|
|
352
|
-
|
|
353
|
-
# removed = self._maybe_remove_label(label)
|
|
354
|
-
# if not removed:
|
|
355
|
-
# break
|
|
356
|
-
if len(collision_scores) > 0:
|
|
357
|
-
best = sorted(collision_scores, key=lambda c: c[0])[0]
|
|
358
|
-
# return
|
|
359
|
-
if best[0] < 1:
|
|
360
|
-
label = plot_text(**best[1])
|
|
361
|
-
add_label(label)
|
|
362
|
-
|
|
363
|
-
def _text(
|
|
396
|
+
def _text_area(
|
|
364
397
|
self,
|
|
365
398
|
ra: float,
|
|
366
399
|
dec: float,
|
|
367
400
|
text: str,
|
|
401
|
+
area,
|
|
368
402
|
hide_on_collision: bool = True,
|
|
369
403
|
force: bool = False,
|
|
370
404
|
clip_on: bool = True,
|
|
371
|
-
|
|
405
|
+
settings: dict = None,
|
|
372
406
|
**kwargs,
|
|
373
407
|
) -> None:
|
|
374
|
-
if not text:
|
|
375
|
-
return
|
|
376
|
-
|
|
377
|
-
x, y = self._prepare_coords(ra, dec)
|
|
378
408
|
kwargs["path_effects"] = kwargs.get("path_effects", [self.text_border])
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
409
|
+
kwargs["va"] = "center"
|
|
410
|
+
kwargs["ha"] = "center"
|
|
411
|
+
|
|
412
|
+
avoid_constellation_lines = settings.get("avoid_constellation_lines", False)
|
|
413
|
+
padding = settings.get("label_padding", 3)
|
|
414
|
+
settings.get("buffer", 0.1)
|
|
415
|
+
max_distance = settings.get("max_distance", 300)
|
|
416
|
+
distance_step_size = settings.get("distance_step_size", 1)
|
|
417
|
+
point_iterations = settings.get("point_generation_max_iterations", 500)
|
|
418
|
+
random_seed = settings.get("seed")
|
|
419
|
+
|
|
420
|
+
attempts = 0
|
|
421
|
+
height = None
|
|
422
|
+
width = None
|
|
423
|
+
bbox = None
|
|
424
|
+
areas = (
|
|
425
|
+
[p for p in area.geoms] if "MultiPolygon" == str(area.geom_type) else [area]
|
|
426
|
+
)
|
|
427
|
+
new_areas = []
|
|
428
|
+
origin = Point(ra, dec)
|
|
429
|
+
|
|
430
|
+
for a in areas:
|
|
431
|
+
unwrapped = unwrap_polygon_360(a)
|
|
432
|
+
# new_buffer = unwrapped.area / 10 * -1 * buffer * self.scale
|
|
433
|
+
# new_buffer = -1 * buffer * self.scale
|
|
434
|
+
# new_poly = unwrapped.buffer(new_buffer)
|
|
435
|
+
new_areas.append(unwrapped)
|
|
436
|
+
|
|
437
|
+
for d in range(0, max_distance, distance_step_size):
|
|
438
|
+
distance = d / 20
|
|
439
|
+
poly = randrange(len(new_areas))
|
|
440
|
+
point = random_point_in_polygon_at_distance(
|
|
441
|
+
new_areas[poly],
|
|
442
|
+
origin_point=origin,
|
|
443
|
+
distance=distance,
|
|
444
|
+
max_iterations=point_iterations,
|
|
445
|
+
seed=random_seed,
|
|
387
446
|
)
|
|
388
|
-
if clip_on:
|
|
389
|
-
label.set_clip_on(True)
|
|
390
|
-
label.set_clip_path(self._background_clip_path)
|
|
391
|
-
return label
|
|
392
447
|
|
|
393
|
-
|
|
448
|
+
if point is None:
|
|
449
|
+
continue
|
|
394
450
|
|
|
395
|
-
|
|
396
|
-
return
|
|
451
|
+
x, y = self._prepare_coords(point.x, point.y)
|
|
397
452
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
453
|
+
if height and width:
|
|
454
|
+
data_xy = self._proj.transform_point(x, y, self._crs)
|
|
455
|
+
display_x, display_y = self.ax.transData.transform(data_xy)
|
|
456
|
+
bbox = (
|
|
457
|
+
display_x - width / 2,
|
|
458
|
+
display_y - height / 2,
|
|
459
|
+
display_x + width / 2,
|
|
460
|
+
display_y + height / 2,
|
|
461
|
+
)
|
|
462
|
+
label = None
|
|
401
463
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
464
|
+
else:
|
|
465
|
+
label = self._text(x, y, text, **kwargs)
|
|
466
|
+
bbox = self._get_label_bbox(label)
|
|
467
|
+
height = bbox[3] - bbox[1]
|
|
468
|
+
width = bbox[2] - bbox[0]
|
|
469
|
+
|
|
470
|
+
is_open = self._is_open_space(
|
|
471
|
+
bbox,
|
|
472
|
+
padding=padding,
|
|
473
|
+
avoid_clipped=clip_on,
|
|
474
|
+
avoid_constellation_collision=avoid_constellation_lines,
|
|
475
|
+
avoid_marker_collisions=hide_on_collision,
|
|
476
|
+
avoid_label_collisions=hide_on_collision,
|
|
477
|
+
)
|
|
405
478
|
|
|
406
|
-
|
|
407
|
-
original_ha = kwargs.pop("ha", None)
|
|
408
|
-
original_offset_x, original_offset_y = kwargs.pop("xytext", (0, 0))
|
|
409
|
-
anchor_fallbacks = self.style.text_anchor_fallbacks
|
|
410
|
-
for i, a in enumerate(anchor_fallbacks):
|
|
411
|
-
d = AnchorPointEnum.from_str(a).as_matplot()
|
|
412
|
-
va, ha = d["va"], d["ha"]
|
|
413
|
-
offset_x, offset_y = original_offset_x, original_offset_y
|
|
414
|
-
if original_ha != ha:
|
|
415
|
-
offset_x *= -1
|
|
479
|
+
# # TODO : remove label if not fully inside area?
|
|
416
480
|
|
|
417
|
-
|
|
418
|
-
offset_y *= -1
|
|
419
|
-
|
|
420
|
-
if ha == "center":
|
|
421
|
-
offset_x = 0
|
|
422
|
-
offset_y = 0
|
|
481
|
+
attempts += 1
|
|
423
482
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
label, remove_on_collision=hide_on_collision, remove_on_clipped=clip_on
|
|
427
|
-
)
|
|
483
|
+
if is_open and label is None:
|
|
484
|
+
label = self._text(x, y, text, **kwargs)
|
|
428
485
|
|
|
429
|
-
if
|
|
486
|
+
if is_open:
|
|
430
487
|
self._add_label_to_rtree(label)
|
|
431
|
-
|
|
488
|
+
return label
|
|
489
|
+
elif label is not None:
|
|
490
|
+
label.remove()
|
|
432
491
|
|
|
433
492
|
@use_style(LabelStyle)
|
|
434
493
|
def text(
|
|
@@ -438,7 +497,6 @@ class BasePlot(ABC):
|
|
|
438
497
|
dec: float,
|
|
439
498
|
style: LabelStyle = None,
|
|
440
499
|
hide_on_collision: bool = True,
|
|
441
|
-
*args,
|
|
442
500
|
**kwargs,
|
|
443
501
|
):
|
|
444
502
|
"""
|
|
@@ -451,7 +509,7 @@ class BasePlot(ABC):
|
|
|
451
509
|
style: Styling of the text
|
|
452
510
|
hide_on_collision: If True, then the text will not be plotted if it collides with another label
|
|
453
511
|
"""
|
|
454
|
-
if not
|
|
512
|
+
if not text:
|
|
455
513
|
return
|
|
456
514
|
|
|
457
515
|
style = style or LabelStyle()
|
|
@@ -462,17 +520,32 @@ class BasePlot(ABC):
|
|
|
462
520
|
if style.offset_y == "auto":
|
|
463
521
|
style.offset_y = 0
|
|
464
522
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
523
|
+
if kwargs.get("area"):
|
|
524
|
+
return self._text_area(
|
|
525
|
+
ra,
|
|
526
|
+
dec,
|
|
527
|
+
text,
|
|
528
|
+
**style.matplot_kwargs(self.scale),
|
|
529
|
+
area=kwargs.pop("area"),
|
|
530
|
+
hide_on_collision=hide_on_collision,
|
|
531
|
+
xycoords="data",
|
|
532
|
+
xytext=(style.offset_x * self.scale, style.offset_y * self.scale),
|
|
533
|
+
textcoords="offset points",
|
|
534
|
+
settings=kwargs.pop("auto_adjust_settings"),
|
|
535
|
+
**kwargs,
|
|
536
|
+
)
|
|
537
|
+
else:
|
|
538
|
+
return self._text_point(
|
|
539
|
+
ra,
|
|
540
|
+
dec,
|
|
541
|
+
text,
|
|
542
|
+
**style.matplot_kwargs(self.scale),
|
|
543
|
+
hide_on_collision=hide_on_collision,
|
|
544
|
+
xycoords="data",
|
|
545
|
+
xytext=(style.offset_x * self.scale, style.offset_y * self.scale),
|
|
546
|
+
textcoords="offset points",
|
|
547
|
+
**kwargs,
|
|
548
|
+
)
|
|
476
549
|
|
|
477
550
|
@property
|
|
478
551
|
def objects(self) -> models.ObjectList:
|
|
@@ -540,23 +613,12 @@ class BasePlot(ABC):
|
|
|
540
613
|
style.zorder
|
|
541
614
|
)
|
|
542
615
|
|
|
543
|
-
def adjust_text(self, ensure_inside_axes: bool = False, **kwargs) -> None:
|
|
544
|
-
"""Adjust all the labels to avoid overlapping. This function uses the [adjustText](https://adjusttext.readthedocs.io/) library.
|
|
545
|
-
|
|
546
|
-
Args:
|
|
547
|
-
ensure_inside_axes: If True, then labels will be forced to stay within the axes
|
|
548
|
-
**kwargs: Any keyword arguments to pass through to [adjustText](https://adjusttext.readthedocs.io/en/latest/#adjustText.adjust_text)
|
|
549
|
-
|
|
550
|
-
"""
|
|
551
|
-
_adjust_text(
|
|
552
|
-
self.labels, ax=self.ax, ensure_inside_axes=ensure_inside_axes, **kwargs
|
|
553
|
-
)
|
|
554
|
-
|
|
555
616
|
def close_fig(self) -> None:
|
|
556
617
|
"""Closes the underlying matplotlib figure."""
|
|
557
618
|
if self.fig:
|
|
558
619
|
plt.close(self.fig)
|
|
559
620
|
|
|
621
|
+
@profile
|
|
560
622
|
def export(self, filename: str, format: str = "png", padding: float = 0, **kwargs):
|
|
561
623
|
"""Exports the plot to an image file.
|
|
562
624
|
|
|
@@ -567,6 +629,7 @@ class BasePlot(ABC):
|
|
|
567
629
|
**kwargs: Any keyword arguments to pass through to matplotlib's `savefig` method
|
|
568
630
|
|
|
569
631
|
"""
|
|
632
|
+
self.logger.debug("Exporting...")
|
|
570
633
|
self.fig.savefig(
|
|
571
634
|
filename,
|
|
572
635
|
format=format,
|
|
@@ -602,18 +665,35 @@ class BasePlot(ABC):
|
|
|
602
665
|
if not skip_bounds_check and not self.in_bounds(ra, dec):
|
|
603
666
|
return
|
|
604
667
|
|
|
668
|
+
# Plot marker
|
|
605
669
|
x, y = self._prepare_coords(ra, dec)
|
|
606
|
-
|
|
670
|
+
style_kwargs = style.marker.matplot_scatter_kwargs(self.scale)
|
|
607
671
|
self.ax.scatter(
|
|
608
672
|
x,
|
|
609
673
|
y,
|
|
610
|
-
**
|
|
674
|
+
**style_kwargs,
|
|
611
675
|
**self._plot_kwargs(),
|
|
612
676
|
clip_on=True,
|
|
613
677
|
clip_path=self._background_clip_path,
|
|
614
678
|
gid=kwargs.get("gid_marker") or "marker",
|
|
615
679
|
)
|
|
616
680
|
|
|
681
|
+
# Add to spatial index
|
|
682
|
+
data_xy = self._proj.transform_point(x, y, self._crs)
|
|
683
|
+
display_x, display_y = self.ax.transData.transform(data_xy)
|
|
684
|
+
if display_x > 0 and display_y > 0:
|
|
685
|
+
radius = style_kwargs.get("s", 1) ** 0.5 / 5
|
|
686
|
+
bbox = np.array(
|
|
687
|
+
(
|
|
688
|
+
display_x - radius,
|
|
689
|
+
display_y - radius,
|
|
690
|
+
display_x + radius,
|
|
691
|
+
display_y + radius,
|
|
692
|
+
)
|
|
693
|
+
)
|
|
694
|
+
self._markers_rtree.insert(0, bbox, None)
|
|
695
|
+
|
|
696
|
+
# Plot label
|
|
617
697
|
if label:
|
|
618
698
|
label_style = style.label
|
|
619
699
|
if label_style.offset_x == "auto" or label_style.offset_y == "auto":
|
|
@@ -708,7 +788,8 @@ class BasePlot(ABC):
|
|
|
708
788
|
Args:
|
|
709
789
|
style: Styling of the Sun. If None, then the plot's style (specified when creating the plot) will be used
|
|
710
790
|
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.
|
|
711
|
-
label: How the Sun will be labeled on the plot
|
|
791
|
+
label: How the Sun will be labeled on the plot
|
|
792
|
+
legend_label: How the sun will be labeled in the legend
|
|
712
793
|
"""
|
|
713
794
|
s = models.Sun.get(
|
|
714
795
|
dt=self.dt, lat=self.lat, lon=self.lon, ephemeris=self._ephemeris_name
|
|
@@ -781,7 +862,6 @@ class BasePlot(ABC):
|
|
|
781
862
|
raise NotImplementedError
|
|
782
863
|
|
|
783
864
|
def _polygon(self, points: list, style: PolygonStyle, **kwargs):
|
|
784
|
-
points = [geod.to_radec(p) for p in points]
|
|
785
865
|
points = [self._prepare_coords(*p) for p in points]
|
|
786
866
|
patch = patches.Polygon(
|
|
787
867
|
points,
|
|
@@ -799,6 +879,7 @@ class BasePlot(ABC):
|
|
|
799
879
|
style: PolygonStyle,
|
|
800
880
|
points: list = None,
|
|
801
881
|
geometry: Polygon = None,
|
|
882
|
+
legend_label: str = None,
|
|
802
883
|
**kwargs,
|
|
803
884
|
):
|
|
804
885
|
"""
|
|
@@ -807,9 +888,11 @@ class BasePlot(ABC):
|
|
|
807
888
|
Must pass in either `points` **or** `geometry` (but not both).
|
|
808
889
|
|
|
809
890
|
Args:
|
|
891
|
+
style: Style of polygon
|
|
810
892
|
points: List of polygon points `[(ra, dec), ...]` - **must be in counterclockwise order**
|
|
811
893
|
geometry: A shapely Polygon. If this value is passed, then the `points` kwarg will be ignored.
|
|
812
|
-
|
|
894
|
+
legend_label: Label for this object in the legend
|
|
895
|
+
|
|
813
896
|
"""
|
|
814
897
|
if points is None and geometry is None:
|
|
815
898
|
raise ValueError("Must pass points or geometry when plotting polygons.")
|
|
@@ -817,8 +900,13 @@ class BasePlot(ABC):
|
|
|
817
900
|
if geometry is not None:
|
|
818
901
|
points = list(zip(*geometry.exterior.coords.xy))
|
|
819
902
|
|
|
820
|
-
|
|
821
|
-
|
|
903
|
+
self._polygon(points, style, gid=kwargs.get("gid") or "polygon")
|
|
904
|
+
|
|
905
|
+
if legend_label is not None:
|
|
906
|
+
self._add_legend_handle_marker(
|
|
907
|
+
legend_label,
|
|
908
|
+
style=style.to_marker_style(symbol=MarkerSymbolEnum.SQUARE),
|
|
909
|
+
)
|
|
822
910
|
|
|
823
911
|
@use_style(PolygonStyle)
|
|
824
912
|
def rectangle(
|
|
@@ -828,6 +916,7 @@ class BasePlot(ABC):
|
|
|
828
916
|
width_degrees: float,
|
|
829
917
|
style: PolygonStyle,
|
|
830
918
|
angle: float = 0,
|
|
919
|
+
legend_label: str = None,
|
|
831
920
|
**kwargs,
|
|
832
921
|
):
|
|
833
922
|
"""Plots a rectangle
|
|
@@ -836,8 +925,9 @@ class BasePlot(ABC):
|
|
|
836
925
|
center: Center of rectangle (ra, dec)
|
|
837
926
|
height_degrees: Height of rectangle (degrees)
|
|
838
927
|
width_degrees: Width of rectangle (degrees)
|
|
839
|
-
angle: Angle of rotation clockwise (degrees)
|
|
840
928
|
style: Style of rectangle
|
|
929
|
+
angle: Angle of rotation clockwise (degrees)
|
|
930
|
+
legend_label: Label for this object in the legend
|
|
841
931
|
"""
|
|
842
932
|
points = geod.rectangle(
|
|
843
933
|
center,
|
|
@@ -847,6 +937,12 @@ class BasePlot(ABC):
|
|
|
847
937
|
)
|
|
848
938
|
self._polygon(points, style, gid=kwargs.get("gid") or "polygon")
|
|
849
939
|
|
|
940
|
+
if legend_label is not None:
|
|
941
|
+
self._add_legend_handle_marker(
|
|
942
|
+
legend_label,
|
|
943
|
+
style=style.to_marker_style(symbol=MarkerSymbolEnum.SQUARE),
|
|
944
|
+
)
|
|
945
|
+
|
|
850
946
|
@use_style(PolygonStyle)
|
|
851
947
|
def ellipse(
|
|
852
948
|
self,
|
|
@@ -858,6 +954,7 @@ class BasePlot(ABC):
|
|
|
858
954
|
num_pts: int = 100,
|
|
859
955
|
start_angle: int = 0,
|
|
860
956
|
end_angle: int = 360,
|
|
957
|
+
legend_label: str = None,
|
|
861
958
|
**kwargs,
|
|
862
959
|
):
|
|
863
960
|
"""Plots an ellipse
|
|
@@ -869,6 +966,9 @@ class BasePlot(ABC):
|
|
|
869
966
|
style: Style of ellipse
|
|
870
967
|
angle: Angle of rotation clockwise (degrees)
|
|
871
968
|
num_pts: Number of points to calculate for the ellipse polygon
|
|
969
|
+
start_angle: Angle to start at
|
|
970
|
+
end_angle: Angle to end at
|
|
971
|
+
legend_label: Label for this object in the legend
|
|
872
972
|
"""
|
|
873
973
|
|
|
874
974
|
points = geod.ellipse(
|
|
@@ -882,6 +982,12 @@ class BasePlot(ABC):
|
|
|
882
982
|
)
|
|
883
983
|
self._polygon(points, style, gid=kwargs.get("gid") or "polygon")
|
|
884
984
|
|
|
985
|
+
if legend_label is not None:
|
|
986
|
+
self._add_legend_handle_marker(
|
|
987
|
+
legend_label,
|
|
988
|
+
style=style.to_marker_style(symbol=MarkerSymbolEnum.ELLIPSE),
|
|
989
|
+
)
|
|
990
|
+
|
|
885
991
|
@use_style(PolygonStyle)
|
|
886
992
|
def circle(
|
|
887
993
|
self,
|
|
@@ -889,6 +995,7 @@ class BasePlot(ABC):
|
|
|
889
995
|
radius_degrees: float,
|
|
890
996
|
style: PolygonStyle,
|
|
891
997
|
num_pts: int = 100,
|
|
998
|
+
legend_label: str = None,
|
|
892
999
|
**kwargs,
|
|
893
1000
|
):
|
|
894
1001
|
"""Plots a circle
|
|
@@ -898,6 +1005,7 @@ class BasePlot(ABC):
|
|
|
898
1005
|
radius_degrees: Radius of circle (degrees)
|
|
899
1006
|
style: Style of circle
|
|
900
1007
|
num_pts: Number of points to calculate for the circle polygon
|
|
1008
|
+
legend_label: Label for this object in the legend
|
|
901
1009
|
"""
|
|
902
1010
|
self.ellipse(
|
|
903
1011
|
center,
|
|
@@ -909,6 +1017,12 @@ class BasePlot(ABC):
|
|
|
909
1017
|
gid=kwargs.get("gid") or "polygon",
|
|
910
1018
|
)
|
|
911
1019
|
|
|
1020
|
+
if legend_label is not None:
|
|
1021
|
+
self._add_legend_handle_marker(
|
|
1022
|
+
legend_label,
|
|
1023
|
+
style=style.to_marker_style(symbol=MarkerSymbolEnum.CIRCLE),
|
|
1024
|
+
)
|
|
1025
|
+
|
|
912
1026
|
@use_style(LineStyle)
|
|
913
1027
|
def line(self, coordinates: list[tuple[float, float]], style: LineStyle, **kwargs):
|
|
914
1028
|
"""Plots a line
|
|
@@ -1154,11 +1268,11 @@ class BasePlot(ABC):
|
|
|
1154
1268
|
inbounds = []
|
|
1155
1269
|
|
|
1156
1270
|
for ra, dec in ecliptic.RA_DECS:
|
|
1157
|
-
x0, y0 = self._prepare_coords(ra, dec)
|
|
1271
|
+
x0, y0 = self._prepare_coords(ra * 15, dec)
|
|
1158
1272
|
x.append(x0)
|
|
1159
1273
|
y.append(y0)
|
|
1160
|
-
if self.in_bounds(ra, dec):
|
|
1161
|
-
inbounds.append((ra, dec))
|
|
1274
|
+
if self.in_bounds(ra * 15, dec):
|
|
1275
|
+
inbounds.append((ra * 15, dec))
|
|
1162
1276
|
|
|
1163
1277
|
self.ax.plot(
|
|
1164
1278
|
x,
|
|
@@ -1193,7 +1307,7 @@ class BasePlot(ABC):
|
|
|
1193
1307
|
# TODO : handle wrapping
|
|
1194
1308
|
|
|
1195
1309
|
for ra in range(25):
|
|
1196
|
-
x0, y0 = self._prepare_coords(ra, 0)
|
|
1310
|
+
x0, y0 = self._prepare_coords(ra * 15, 0)
|
|
1197
1311
|
x.append(x0)
|
|
1198
1312
|
y.append(y0)
|
|
1199
1313
|
|