kerykeion 5.0.0a9__py3-none-any.whl → 5.1.8__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 kerykeion might be problematic. Click here for more details.
- kerykeion/__init__.py +50 -9
- kerykeion/aspects/__init__.py +5 -2
- kerykeion/aspects/aspects_factory.py +568 -0
- kerykeion/aspects/aspects_utils.py +78 -11
- kerykeion/astrological_subject_factory.py +1032 -275
- kerykeion/backword.py +820 -0
- kerykeion/chart_data_factory.py +552 -0
- kerykeion/charts/chart_drawer.py +2661 -0
- kerykeion/charts/charts_utils.py +652 -399
- kerykeion/charts/draw_planets.py +603 -353
- kerykeion/charts/templates/aspect_grid_only.xml +326 -198
- kerykeion/charts/templates/chart.xml +306 -256
- kerykeion/charts/templates/wheel_only.xml +330 -200
- kerykeion/charts/themes/black-and-white.css +148 -0
- kerykeion/charts/themes/classic.css +11 -0
- kerykeion/charts/themes/dark-high-contrast.css +11 -0
- kerykeion/charts/themes/dark.css +11 -0
- kerykeion/charts/themes/light.css +11 -0
- kerykeion/charts/themes/strawberry.css +10 -0
- kerykeion/composite_subject_factory.py +232 -13
- kerykeion/ephemeris_data_factory.py +443 -0
- kerykeion/fetch_geonames.py +78 -21
- kerykeion/house_comparison/__init__.py +4 -1
- kerykeion/house_comparison/house_comparison_factory.py +52 -19
- kerykeion/house_comparison/house_comparison_utils.py +37 -9
- kerykeion/kr_types/__init__.py +66 -6
- kerykeion/kr_types/chart_template_model.py +20 -0
- kerykeion/kr_types/kerykeion_exception.py +15 -9
- kerykeion/kr_types/kr_literals.py +14 -160
- kerykeion/kr_types/kr_models.py +14 -291
- kerykeion/kr_types/settings_models.py +15 -167
- kerykeion/planetary_return_factory.py +545 -40
- kerykeion/relationship_score_factory.py +137 -63
- kerykeion/report.py +749 -64
- kerykeion/schemas/__init__.py +106 -0
- kerykeion/schemas/chart_template_model.py +367 -0
- kerykeion/schemas/kerykeion_exception.py +20 -0
- kerykeion/schemas/kr_literals.py +181 -0
- kerykeion/schemas/kr_models.py +603 -0
- kerykeion/schemas/settings_models.py +188 -0
- kerykeion/settings/__init__.py +20 -1
- kerykeion/settings/chart_defaults.py +444 -0
- kerykeion/settings/config_constants.py +88 -12
- kerykeion/settings/kerykeion_settings.py +32 -75
- kerykeion/settings/translation_strings.py +1499 -0
- kerykeion/settings/translations.py +74 -0
- kerykeion/sweph/ast136/s136108s.se1 +0 -0
- kerykeion/sweph/ast136/s136199s.se1 +0 -0
- kerykeion/sweph/ast136/s136472s.se1 +0 -0
- kerykeion/sweph/ast28/se28978s.se1 +0 -0
- kerykeion/sweph/ast50/se50000s.se1 +0 -0
- kerykeion/sweph/ast90/se90377s.se1 +0 -0
- kerykeion/sweph/ast90/se90482s.se1 +0 -0
- kerykeion/sweph/sefstars.txt +1602 -0
- kerykeion/transits_time_range_factory.py +302 -0
- kerykeion/utilities.py +289 -204
- kerykeion-5.1.8.dist-info/METADATA +1793 -0
- kerykeion-5.1.8.dist-info/RECORD +63 -0
- kerykeion/aspects/natal_aspects.py +0 -181
- kerykeion/aspects/synastry_aspects.py +0 -141
- kerykeion/aspects/transits_time_range.py +0 -41
- kerykeion/charts/draw_planets_v2.py +0 -649
- kerykeion/charts/draw_planets_v3.py +0 -679
- kerykeion/charts/kerykeion_chart_svg.py +0 -2038
- kerykeion/enums.py +0 -57
- kerykeion/ephemeris_data.py +0 -238
- kerykeion/house_comparison/house_comparison_models.py +0 -38
- kerykeion/kr_types/chart_types.py +0 -106
- kerykeion/settings/kr.config.json +0 -1304
- kerykeion/settings/legacy/__init__.py +0 -0
- kerykeion/settings/legacy/legacy_celestial_points_settings.py +0 -299
- kerykeion/settings/legacy/legacy_chart_aspects_settings.py +0 -71
- kerykeion/settings/legacy/legacy_color_settings.py +0 -42
- kerykeion/transits_time_range.py +0 -128
- kerykeion-5.0.0a9.dist-info/METADATA +0 -636
- kerykeion-5.0.0a9.dist-info/RECORD +0 -55
- kerykeion-5.0.0a9.dist-info/entry_points.txt +0 -2
- {kerykeion-5.0.0a9.dist-info → kerykeion-5.1.8.dist-info}/WHEEL +0 -0
- {kerykeion-5.0.0a9.dist-info → kerykeion-5.1.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,679 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
TODO: Not stable at all, check it very well before using it!
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from dataclasses import dataclass
|
|
6
|
-
from enum import Enum
|
|
7
|
-
from typing import Union, Optional, List, Dict, Tuple, get_args
|
|
8
|
-
import logging
|
|
9
|
-
|
|
10
|
-
from kerykeion.charts.charts_utils import degreeDiff, sliceToX, sliceToY, convert_decimal_to_degree_string
|
|
11
|
-
from kerykeion.kr_types import KerykeionException, ChartType, KerykeionPointModel
|
|
12
|
-
from kerykeion.kr_types.settings_models import KerykeionSettingsCelestialPointModel
|
|
13
|
-
from kerykeion.kr_types.kr_literals import Houses
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class ChartRadius(Enum):
|
|
17
|
-
"""Standard radius values for different chart elements."""
|
|
18
|
-
ANGLE_TRANSIT = 76
|
|
19
|
-
ANGLE_NATAL = 40
|
|
20
|
-
ANGLE_EXTERNAL_NATAL = 30
|
|
21
|
-
PLANET_PRIMARY = 94
|
|
22
|
-
PLANET_ALTERNATE = 74
|
|
23
|
-
PLANET_TRANSIT_PRIMARY = 130
|
|
24
|
-
PLANET_TRANSIT_ALTERNATE = 110
|
|
25
|
-
PLANET_EXTERNAL_PRIMARY = 84
|
|
26
|
-
PLANET_EXTERNAL_ALTERNATE = 64
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class ChartConstants:
|
|
30
|
-
"""Configuration constants for chart drawing."""
|
|
31
|
-
PLANET_GROUPING_THRESHOLD = 3.4
|
|
32
|
-
SECONDARY_GROUPING_THRESHOLD = 2.5
|
|
33
|
-
ANGLE_INDEX_RANGE = (22, 27) # ASC, MC, DSC, IC indices
|
|
34
|
-
SYMBOL_SCALE_FACTOR = 0.8
|
|
35
|
-
SYMBOL_SIZE = 12
|
|
36
|
-
MAX_DEGREE_DISTANCE = 360.0
|
|
37
|
-
|
|
38
|
-
# Line styling
|
|
39
|
-
LINE_OPACITY_PRIMARY = 0.3
|
|
40
|
-
LINE_OPACITY_SECONDARY = 0.5
|
|
41
|
-
LINE_WIDTH_THIN = 1
|
|
42
|
-
LINE_WIDTH_THICK = 2
|
|
43
|
-
|
|
44
|
-
# Text styling
|
|
45
|
-
TEXT_SIZE = 10
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@dataclass
|
|
49
|
-
class PointPosition:
|
|
50
|
-
"""Represents a celestial point's position and distance information."""
|
|
51
|
-
index: int
|
|
52
|
-
position_index: int
|
|
53
|
-
distance_to_prev: float
|
|
54
|
-
distance_to_next: float
|
|
55
|
-
label: str
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
@dataclass
|
|
59
|
-
class ChartConfiguration:
|
|
60
|
-
"""Configuration for chart drawing parameters."""
|
|
61
|
-
radius: float
|
|
62
|
-
third_circle_radius: float
|
|
63
|
-
first_house_degree: float
|
|
64
|
-
seventh_house_degree: float
|
|
65
|
-
chart_type: ChartType
|
|
66
|
-
scale_factor: float = 1.0
|
|
67
|
-
|
|
68
|
-
def __post_init__(self):
|
|
69
|
-
"""Set scale factor based on chart type."""
|
|
70
|
-
if self.chart_type in ["Transit", "Synastry", "Return", "ExternalNatal"]:
|
|
71
|
-
self.scale_factor = ChartConstants.SYMBOL_SCALE_FACTOR
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
class CelestialPointGrouper:
|
|
75
|
-
"""Handles grouping of celestial points that are close together."""
|
|
76
|
-
|
|
77
|
-
def __init__(self, threshold: float = ChartConstants.PLANET_GROUPING_THRESHOLD):
|
|
78
|
-
self.threshold = threshold
|
|
79
|
-
|
|
80
|
-
def create_position_mapping(
|
|
81
|
-
self,
|
|
82
|
-
celestial_points: List[KerykeionPointModel],
|
|
83
|
-
settings: List[KerykeionSettingsCelestialPointModel]
|
|
84
|
-
) -> Tuple[Dict[float, int], List[float]]:
|
|
85
|
-
"""Create mapping from absolute positions to point indices."""
|
|
86
|
-
position_index_map = {}
|
|
87
|
-
for i, point in enumerate(celestial_points):
|
|
88
|
-
position_index_map[point.abs_pos] = i
|
|
89
|
-
logging.debug(f"Point {settings[i]['label']}: index {i}, degree {point.abs_pos}")
|
|
90
|
-
|
|
91
|
-
return position_index_map, sorted(position_index_map.keys())
|
|
92
|
-
|
|
93
|
-
def calculate_distances(
|
|
94
|
-
self,
|
|
95
|
-
sorted_positions: List[float],
|
|
96
|
-
position_index_map: Dict[float, int],
|
|
97
|
-
abs_positions: List[float]
|
|
98
|
-
) -> List[PointPosition]:
|
|
99
|
-
"""Calculate distances between adjacent points."""
|
|
100
|
-
point_positions = []
|
|
101
|
-
|
|
102
|
-
for position_idx, abs_position in enumerate(sorted_positions):
|
|
103
|
-
point_idx = position_index_map[abs_position]
|
|
104
|
-
|
|
105
|
-
if len(sorted_positions) == 1:
|
|
106
|
-
# Single point case
|
|
107
|
-
distance_to_prev = distance_to_next = ChartConstants.MAX_DEGREE_DISTANCE
|
|
108
|
-
else:
|
|
109
|
-
prev_idx, next_idx = self._get_adjacent_indices(
|
|
110
|
-
position_idx, sorted_positions, position_index_map
|
|
111
|
-
)
|
|
112
|
-
distance_to_prev = degreeDiff(abs_positions[prev_idx], abs_positions[point_idx])
|
|
113
|
-
distance_to_next = degreeDiff(abs_positions[next_idx], abs_positions[point_idx])
|
|
114
|
-
|
|
115
|
-
point_positions.append(PointPosition(
|
|
116
|
-
index=point_idx,
|
|
117
|
-
position_index=position_idx,
|
|
118
|
-
distance_to_prev=distance_to_prev,
|
|
119
|
-
distance_to_next=distance_to_next,
|
|
120
|
-
label=f"point_{point_idx}" # Will be updated by caller
|
|
121
|
-
))
|
|
122
|
-
|
|
123
|
-
return point_positions
|
|
124
|
-
|
|
125
|
-
def _get_adjacent_indices(
|
|
126
|
-
self,
|
|
127
|
-
position_idx: int,
|
|
128
|
-
sorted_positions: List[float],
|
|
129
|
-
position_index_map: Dict[float, int]
|
|
130
|
-
) -> Tuple[int, int]:
|
|
131
|
-
"""Get indices of previous and next points."""
|
|
132
|
-
total_positions = len(sorted_positions)
|
|
133
|
-
|
|
134
|
-
if position_idx == 0:
|
|
135
|
-
prev_position = sorted_positions[-1]
|
|
136
|
-
next_position = sorted_positions[1]
|
|
137
|
-
elif position_idx == total_positions - 1:
|
|
138
|
-
prev_position = sorted_positions[position_idx - 1]
|
|
139
|
-
next_position = sorted_positions[0]
|
|
140
|
-
else:
|
|
141
|
-
prev_position = sorted_positions[position_idx - 1]
|
|
142
|
-
next_position = sorted_positions[position_idx + 1]
|
|
143
|
-
|
|
144
|
-
return position_index_map[prev_position], position_index_map[next_position]
|
|
145
|
-
|
|
146
|
-
def identify_groups(self, point_positions: List[PointPosition]) -> List[List[PointPosition]]:
|
|
147
|
-
"""Identify groups of points that are close together."""
|
|
148
|
-
groups = []
|
|
149
|
-
current_group = []
|
|
150
|
-
|
|
151
|
-
for point_pos in point_positions:
|
|
152
|
-
if point_pos.distance_to_next < self.threshold:
|
|
153
|
-
current_group.append(point_pos)
|
|
154
|
-
else:
|
|
155
|
-
if current_group:
|
|
156
|
-
current_group.append(point_pos)
|
|
157
|
-
groups.append(current_group)
|
|
158
|
-
current_group = []
|
|
159
|
-
|
|
160
|
-
# Handle case where last group wraps around
|
|
161
|
-
if current_group and groups and point_positions[0] in groups[0]:
|
|
162
|
-
groups[0] = current_group + groups[0]
|
|
163
|
-
elif current_group:
|
|
164
|
-
groups.append(current_group)
|
|
165
|
-
|
|
166
|
-
return groups
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
class PositionAdjuster:
|
|
170
|
-
"""Calculates position adjustments to prevent overlapping points."""
|
|
171
|
-
|
|
172
|
-
def __init__(self, threshold: float = ChartConstants.PLANET_GROUPING_THRESHOLD):
|
|
173
|
-
self.threshold = threshold
|
|
174
|
-
|
|
175
|
-
def calculate_adjustments(
|
|
176
|
-
self,
|
|
177
|
-
groups: List[List[PointPosition]],
|
|
178
|
-
total_points: int
|
|
179
|
-
) -> List[float]:
|
|
180
|
-
"""Calculate position adjustments for all points."""
|
|
181
|
-
adjustments = [0.0] * total_points
|
|
182
|
-
|
|
183
|
-
for group in groups:
|
|
184
|
-
if len(group) == 2:
|
|
185
|
-
self._handle_two_point_group(group, adjustments)
|
|
186
|
-
elif len(group) >= 3:
|
|
187
|
-
self._handle_multi_point_group(group, adjustments)
|
|
188
|
-
|
|
189
|
-
return adjustments
|
|
190
|
-
|
|
191
|
-
def _handle_two_point_group(
|
|
192
|
-
self,
|
|
193
|
-
group: List[PointPosition],
|
|
194
|
-
adjustments: List[float]
|
|
195
|
-
) -> None:
|
|
196
|
-
"""Handle positioning for a group of two points."""
|
|
197
|
-
point_a, point_b = group[0], group[1]
|
|
198
|
-
|
|
199
|
-
# Check available space around the group
|
|
200
|
-
if (point_a.distance_to_prev > 2 * self.threshold and
|
|
201
|
-
point_b.distance_to_next > 2 * self.threshold):
|
|
202
|
-
# Both points have room
|
|
203
|
-
offset = (self.threshold - point_a.distance_to_next) / 2
|
|
204
|
-
adjustments[point_a.position_index] = -offset
|
|
205
|
-
adjustments[point_b.position_index] = +offset
|
|
206
|
-
elif point_a.distance_to_prev > 2 * self.threshold:
|
|
207
|
-
# Only first point has room
|
|
208
|
-
adjustments[point_a.position_index] = -self.threshold
|
|
209
|
-
elif point_b.distance_to_next > 2 * self.threshold:
|
|
210
|
-
# Only second point has room
|
|
211
|
-
adjustments[point_b.position_index] = +self.threshold
|
|
212
|
-
|
|
213
|
-
def _handle_multi_point_group(
|
|
214
|
-
self,
|
|
215
|
-
group: List[PointPosition],
|
|
216
|
-
adjustments: List[float]
|
|
217
|
-
) -> None:
|
|
218
|
-
"""Handle positioning for groups of three or more points."""
|
|
219
|
-
group_size = len(group)
|
|
220
|
-
|
|
221
|
-
# Calculate available and needed space
|
|
222
|
-
available_space = group[0].distance_to_prev
|
|
223
|
-
for point in group:
|
|
224
|
-
available_space += point.distance_to_next
|
|
225
|
-
|
|
226
|
-
needed_space = 3 * self.threshold + 1.2 * (group_size - 1) * self.threshold
|
|
227
|
-
|
|
228
|
-
if available_space > needed_space:
|
|
229
|
-
# Distribute points evenly
|
|
230
|
-
spacing = 1.2 * self.threshold
|
|
231
|
-
start_offset = (available_space - needed_space) / 2
|
|
232
|
-
|
|
233
|
-
for i, point in enumerate(group):
|
|
234
|
-
adjustments[point.position_index] = start_offset + i * spacing - group[0].distance_to_prev
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
class RadiusCalculator:
|
|
238
|
-
"""Calculates appropriate radius for different point types and chart types."""
|
|
239
|
-
|
|
240
|
-
@staticmethod
|
|
241
|
-
def get_point_radius(
|
|
242
|
-
point_idx: int,
|
|
243
|
-
chart_type: ChartType,
|
|
244
|
-
is_alternate: bool = False
|
|
245
|
-
) -> int:
|
|
246
|
-
"""Get radius for a celestial point based on its type and chart context."""
|
|
247
|
-
is_angle = ChartConstants.ANGLE_INDEX_RANGE[0] < point_idx < ChartConstants.ANGLE_INDEX_RANGE[1]
|
|
248
|
-
|
|
249
|
-
if chart_type in ["Transit", "Synastry", "Return"]:
|
|
250
|
-
if is_angle:
|
|
251
|
-
return ChartRadius.ANGLE_TRANSIT.value
|
|
252
|
-
return (ChartRadius.PLANET_TRANSIT_ALTERNATE.value if is_alternate
|
|
253
|
-
else ChartRadius.PLANET_TRANSIT_PRIMARY.value)
|
|
254
|
-
|
|
255
|
-
elif chart_type == "ExternalNatal":
|
|
256
|
-
if is_angle:
|
|
257
|
-
return ChartRadius.ANGLE_EXTERNAL_NATAL.value
|
|
258
|
-
return (ChartRadius.PLANET_EXTERNAL_ALTERNATE.value if is_alternate
|
|
259
|
-
else ChartRadius.PLANET_EXTERNAL_PRIMARY.value)
|
|
260
|
-
|
|
261
|
-
else: # Natal chart
|
|
262
|
-
if is_angle:
|
|
263
|
-
return ChartRadius.ANGLE_NATAL.value
|
|
264
|
-
return (ChartRadius.PLANET_ALTERNATE.value if is_alternate
|
|
265
|
-
else ChartRadius.PLANET_PRIMARY.value)
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
class SVGRenderer:
|
|
269
|
-
"""Handles SVG generation for celestial points."""
|
|
270
|
-
|
|
271
|
-
def __init__(self, config: ChartConfiguration):
|
|
272
|
-
self.config = config
|
|
273
|
-
|
|
274
|
-
def calculate_offset(self, point_degree: float, adjustment: float = 0) -> float:
|
|
275
|
-
"""Calculate the angular offset for positioning a point."""
|
|
276
|
-
return (-self.config.seventh_house_degree) + point_degree + adjustment
|
|
277
|
-
|
|
278
|
-
def generate_point_svg(
|
|
279
|
-
self,
|
|
280
|
-
point: KerykeionPointModel,
|
|
281
|
-
x: float,
|
|
282
|
-
y: float,
|
|
283
|
-
point_name: str
|
|
284
|
-
) -> str:
|
|
285
|
-
"""Generate SVG element for a celestial point."""
|
|
286
|
-
scale = self.config.scale_factor
|
|
287
|
-
transform_offset = ChartConstants.SYMBOL_SIZE * scale
|
|
288
|
-
|
|
289
|
-
svg_parts = [
|
|
290
|
-
f'<g kr:node="ChartPoint" kr:house="{point.house}" kr:sign="{point.sign}" ',
|
|
291
|
-
f'kr:slug="{point.name}" transform="translate(-{transform_offset},-{transform_offset}) scale({scale})">',
|
|
292
|
-
f'<use x="{x / scale}" y="{y / scale}" xlink:href="#{point_name}" />',
|
|
293
|
-
'</g>'
|
|
294
|
-
]
|
|
295
|
-
|
|
296
|
-
return ''.join(svg_parts)
|
|
297
|
-
|
|
298
|
-
def draw_external_natal_lines(
|
|
299
|
-
self,
|
|
300
|
-
point_radius: float,
|
|
301
|
-
true_offset: float,
|
|
302
|
-
adjusted_offset: float,
|
|
303
|
-
color: str
|
|
304
|
-
) -> str:
|
|
305
|
-
"""Draw connecting lines for ExternalNatal chart type."""
|
|
306
|
-
lines = []
|
|
307
|
-
|
|
308
|
-
# First line segment
|
|
309
|
-
x1 = sliceToX(0, self.config.radius - self.config.third_circle_radius, true_offset) + self.config.third_circle_radius
|
|
310
|
-
y1 = sliceToY(0, self.config.radius - self.config.third_circle_radius, true_offset) + self.config.third_circle_radius
|
|
311
|
-
x2 = sliceToX(0, self.config.radius - point_radius - 30, true_offset) + point_radius + 30
|
|
312
|
-
y2 = sliceToY(0, self.config.radius - point_radius - 30, true_offset) + point_radius + 30
|
|
313
|
-
|
|
314
|
-
lines.append(f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" '
|
|
315
|
-
f'style="stroke-width:{ChartConstants.LINE_WIDTH_THIN}px;stroke:{color};'
|
|
316
|
-
f'stroke-opacity:{ChartConstants.LINE_OPACITY_PRIMARY};"/>')
|
|
317
|
-
|
|
318
|
-
# Second line segment
|
|
319
|
-
x3 = sliceToX(0, self.config.radius - point_radius - 10, adjusted_offset) + point_radius + 10
|
|
320
|
-
y3 = sliceToY(0, self.config.radius - point_radius - 10, adjusted_offset) + point_radius + 10
|
|
321
|
-
|
|
322
|
-
lines.append(f'<line x1="{x2}" y1="{y2}" x2="{x3}" y2="{y3}" '
|
|
323
|
-
f'style="stroke-width:{ChartConstants.LINE_WIDTH_THIN}px;stroke:{color};'
|
|
324
|
-
f'stroke-opacity:{ChartConstants.LINE_OPACITY_SECONDARY};"/>')
|
|
325
|
-
|
|
326
|
-
return '\n'.join(lines) + '\n'
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
class SecondaryPointsRenderer:
|
|
330
|
-
"""Handles rendering of secondary points (transit, synastry, return)."""
|
|
331
|
-
|
|
332
|
-
def __init__(self, config: ChartConfiguration):
|
|
333
|
-
self.config = config
|
|
334
|
-
self.grouper = CelestialPointGrouper(ChartConstants.SECONDARY_GROUPING_THRESHOLD)
|
|
335
|
-
|
|
336
|
-
def draw_secondary_points(
|
|
337
|
-
self,
|
|
338
|
-
points_abs_positions: List[float],
|
|
339
|
-
points_rel_positions: List[float],
|
|
340
|
-
points_settings: List[KerykeionSettingsCelestialPointModel],
|
|
341
|
-
exclude_points: List[str]
|
|
342
|
-
) -> str:
|
|
343
|
-
"""Draw all secondary celestial points."""
|
|
344
|
-
if not points_abs_positions:
|
|
345
|
-
return ""
|
|
346
|
-
|
|
347
|
-
# Filter out excluded points
|
|
348
|
-
filtered_indices = [
|
|
349
|
-
i for i, setting in enumerate(points_settings)
|
|
350
|
-
if self.config.chart_type != "Transit" or setting["name"] not in exclude_points
|
|
351
|
-
]
|
|
352
|
-
|
|
353
|
-
if not filtered_indices:
|
|
354
|
-
return ""
|
|
355
|
-
|
|
356
|
-
# Calculate position adjustments for grouping
|
|
357
|
-
adjustments = self._calculate_secondary_adjustments(
|
|
358
|
-
filtered_indices, points_abs_positions, points_settings
|
|
359
|
-
)
|
|
360
|
-
|
|
361
|
-
# Render each secondary point
|
|
362
|
-
output_parts = []
|
|
363
|
-
alternate_position = False
|
|
364
|
-
|
|
365
|
-
for idx in filtered_indices:
|
|
366
|
-
point_svg = self._render_single_secondary_point(
|
|
367
|
-
idx, points_abs_positions, points_rel_positions,
|
|
368
|
-
points_settings, adjustments, alternate_position
|
|
369
|
-
)
|
|
370
|
-
output_parts.append(point_svg)
|
|
371
|
-
alternate_position = not alternate_position
|
|
372
|
-
|
|
373
|
-
return ''.join(output_parts)
|
|
374
|
-
|
|
375
|
-
def _calculate_secondary_adjustments(
|
|
376
|
-
self,
|
|
377
|
-
indices: List[int],
|
|
378
|
-
positions: List[float],
|
|
379
|
-
settings: List[KerykeionSettingsCelestialPointModel]
|
|
380
|
-
) -> Dict[int, float]:
|
|
381
|
-
"""Calculate position adjustments for secondary points."""
|
|
382
|
-
# Create position mapping for filtered indices
|
|
383
|
-
position_map = {positions[i]: i for i in indices}
|
|
384
|
-
sorted_positions = sorted(position_map.keys())
|
|
385
|
-
|
|
386
|
-
# Find groups
|
|
387
|
-
groups = []
|
|
388
|
-
current_group = []
|
|
389
|
-
|
|
390
|
-
for i, pos in enumerate(sorted_positions):
|
|
391
|
-
point_idx = position_map[pos]
|
|
392
|
-
next_pos = sorted_positions[(i + 1) % len(sorted_positions)]
|
|
393
|
-
next_idx = position_map[next_pos]
|
|
394
|
-
|
|
395
|
-
distance = degreeDiff(positions[point_idx], positions[next_idx])
|
|
396
|
-
|
|
397
|
-
if distance <= self.grouper.threshold:
|
|
398
|
-
if not current_group:
|
|
399
|
-
current_group = [point_idx]
|
|
400
|
-
current_group.append(next_idx)
|
|
401
|
-
else:
|
|
402
|
-
if current_group:
|
|
403
|
-
groups.append(current_group)
|
|
404
|
-
current_group = []
|
|
405
|
-
|
|
406
|
-
if current_group:
|
|
407
|
-
groups.append(current_group)
|
|
408
|
-
|
|
409
|
-
# Calculate adjustments
|
|
410
|
-
adjustments = {i: 0.0 for i in indices}
|
|
411
|
-
|
|
412
|
-
for group in groups:
|
|
413
|
-
if len(group) == 2:
|
|
414
|
-
adjustments[group[0]] = -1.0
|
|
415
|
-
adjustments[group[1]] = 1.0
|
|
416
|
-
elif len(group) == 3:
|
|
417
|
-
adjustments[group[0]] = -1.5
|
|
418
|
-
adjustments[group[1]] = 0.0
|
|
419
|
-
adjustments[group[2]] = 1.5
|
|
420
|
-
elif len(group) >= 4:
|
|
421
|
-
for j, point_idx in enumerate(group):
|
|
422
|
-
adjustments[point_idx] = -2.0 + j * (4.0 / (len(group) - 1))
|
|
423
|
-
|
|
424
|
-
return adjustments
|
|
425
|
-
|
|
426
|
-
def _render_single_secondary_point(
|
|
427
|
-
self,
|
|
428
|
-
point_idx: int,
|
|
429
|
-
abs_positions: List[float],
|
|
430
|
-
rel_positions: List[float],
|
|
431
|
-
settings: List[KerykeionSettingsCelestialPointModel],
|
|
432
|
-
adjustments: Dict[int, float],
|
|
433
|
-
is_alternate: bool
|
|
434
|
-
) -> str:
|
|
435
|
-
"""Render a single secondary point with symbol, line, and degree text."""
|
|
436
|
-
# Determine radius
|
|
437
|
-
is_angle = ChartConstants.ANGLE_INDEX_RANGE[0] < point_idx < ChartConstants.ANGLE_INDEX_RANGE[1]
|
|
438
|
-
point_radius = 9 if is_angle else (18 if is_alternate else 26)
|
|
439
|
-
|
|
440
|
-
# Calculate position
|
|
441
|
-
point_offset = self._calculate_secondary_offset(abs_positions[point_idx])
|
|
442
|
-
|
|
443
|
-
# Generate SVG components
|
|
444
|
-
symbol_svg = self._generate_secondary_symbol(point_idx, point_radius, point_offset, settings)
|
|
445
|
-
line_svg = self._generate_secondary_line(point_idx, point_offset, settings)
|
|
446
|
-
text_svg = self._generate_secondary_text(
|
|
447
|
-
point_idx, abs_positions, rel_positions, settings, adjustments, point_offset
|
|
448
|
-
)
|
|
449
|
-
|
|
450
|
-
return symbol_svg + line_svg + text_svg
|
|
451
|
-
|
|
452
|
-
def _calculate_secondary_offset(self, abs_position: float) -> float:
|
|
453
|
-
"""Calculate offset for secondary point positioning."""
|
|
454
|
-
zero_point = 360 - self.config.seventh_house_degree
|
|
455
|
-
offset = zero_point + abs_position
|
|
456
|
-
return offset - 360 if offset > 360 else offset
|
|
457
|
-
|
|
458
|
-
def _generate_secondary_symbol(
|
|
459
|
-
self, point_idx: int, radius: int, offset: float,
|
|
460
|
-
settings: List[KerykeionSettingsCelestialPointModel]
|
|
461
|
-
) -> str:
|
|
462
|
-
"""Generate SVG for secondary point symbol."""
|
|
463
|
-
x = sliceToX(0, self.config.radius - radius, offset) + radius
|
|
464
|
-
y = sliceToY(0, self.config.radius - radius, offset) + radius
|
|
465
|
-
|
|
466
|
-
return (f'<g class="transit-planet-name" transform="translate(-6,-6)">'
|
|
467
|
-
f'<g transform="scale(0.5)">'
|
|
468
|
-
f'<use x="{x*2}" y="{y*2}" xlink:href="#{settings[point_idx]["name"]}" />'
|
|
469
|
-
f'</g></g>')
|
|
470
|
-
|
|
471
|
-
def _generate_secondary_line(
|
|
472
|
-
self, point_idx: int, offset: float,
|
|
473
|
-
settings: List[KerykeionSettingsCelestialPointModel]
|
|
474
|
-
) -> str:
|
|
475
|
-
"""Generate connecting line for secondary point."""
|
|
476
|
-
x1 = sliceToX(0, self.config.radius + 3, offset) - 3
|
|
477
|
-
y1 = sliceToY(0, self.config.radius + 3, offset) - 3
|
|
478
|
-
x2 = sliceToX(0, self.config.radius - 3, offset) + 3
|
|
479
|
-
y2 = sliceToY(0, self.config.radius - 3, offset) + 3
|
|
480
|
-
|
|
481
|
-
color = settings[point_idx]["color"]
|
|
482
|
-
return (f'<line class="transit-planet-line" x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" '
|
|
483
|
-
f'style="stroke: {color}; stroke-width: {ChartConstants.LINE_WIDTH_THIN}px; '
|
|
484
|
-
f'stroke-opacity:.8;"/>')
|
|
485
|
-
|
|
486
|
-
def _generate_secondary_text(
|
|
487
|
-
self,
|
|
488
|
-
point_idx: int,
|
|
489
|
-
abs_positions: List[float],
|
|
490
|
-
rel_positions: List[float],
|
|
491
|
-
settings: List[KerykeionSettingsCelestialPointModel],
|
|
492
|
-
adjustments: Dict[int, float],
|
|
493
|
-
point_offset: float
|
|
494
|
-
) -> str:
|
|
495
|
-
"""Generate degree text for secondary point."""
|
|
496
|
-
# Calculate rotation and text anchor
|
|
497
|
-
rotation = self.config.first_house_degree - abs_positions[point_idx]
|
|
498
|
-
text_anchor = "end"
|
|
499
|
-
|
|
500
|
-
# Adjust for readability
|
|
501
|
-
if -270 < rotation < -90:
|
|
502
|
-
rotation += 180.0
|
|
503
|
-
text_anchor = "start"
|
|
504
|
-
elif 90 < rotation < 270:
|
|
505
|
-
rotation -= 180.0
|
|
506
|
-
text_anchor = "start"
|
|
507
|
-
|
|
508
|
-
# Position text
|
|
509
|
-
x_offset = 1 if text_anchor == "end" else -1
|
|
510
|
-
adjusted_offset = point_offset + adjustments[point_idx]
|
|
511
|
-
text_radius = -3.0
|
|
512
|
-
|
|
513
|
-
deg_x = sliceToX(0, self.config.radius - text_radius, adjusted_offset + x_offset) + text_radius
|
|
514
|
-
deg_y = sliceToY(0, self.config.radius - text_radius, adjusted_offset + x_offset) + text_radius
|
|
515
|
-
|
|
516
|
-
# Format degree text
|
|
517
|
-
degree_text = convert_decimal_to_degree_string(rel_positions[point_idx], format_type="1")
|
|
518
|
-
color = settings[point_idx]["color"]
|
|
519
|
-
|
|
520
|
-
return (f'<g transform="translate({deg_x},{deg_y})">'
|
|
521
|
-
f'<text transform="rotate({rotation})" text-anchor="{text_anchor}" '
|
|
522
|
-
f'style="fill: {color}; font-size: {ChartConstants.TEXT_SIZE}px;">{degree_text}</text>'
|
|
523
|
-
f'</g>')
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
def _validate_chart_inputs(
|
|
527
|
-
chart_type: ChartType,
|
|
528
|
-
secondary_points: Optional[List[KerykeionPointModel]]
|
|
529
|
-
) -> None:
|
|
530
|
-
"""Validate that required secondary points are provided for chart types that need them."""
|
|
531
|
-
if _requires_secondary_points(chart_type) and secondary_points is None:
|
|
532
|
-
raise KerykeionException(f"Secondary celestial points are required for {chart_type} charts")
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
def _requires_secondary_points(chart_type: ChartType) -> bool:
|
|
536
|
-
"""Check if chart type requires secondary celestial points."""
|
|
537
|
-
return chart_type in ["Transit", "Synastry", "Return"]
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
def _draw_main_points(
|
|
541
|
-
celestial_points: List[KerykeionPointModel],
|
|
542
|
-
settings: List[KerykeionSettingsCelestialPointModel],
|
|
543
|
-
config: ChartConfiguration,
|
|
544
|
-
grouper: CelestialPointGrouper,
|
|
545
|
-
adjuster: PositionAdjuster,
|
|
546
|
-
renderer: SVGRenderer
|
|
547
|
-
) -> str:
|
|
548
|
-
"""Draw the main celestial points with proper grouping and positioning."""
|
|
549
|
-
# Create position mapping and calculate distances
|
|
550
|
-
position_map, sorted_positions = grouper.create_position_mapping(celestial_points, settings)
|
|
551
|
-
abs_positions = [p.abs_pos for p in celestial_points]
|
|
552
|
-
|
|
553
|
-
point_positions = grouper.calculate_distances(sorted_positions, position_map, abs_positions)
|
|
554
|
-
|
|
555
|
-
# Update labels
|
|
556
|
-
for point_pos in point_positions:
|
|
557
|
-
point_pos.label = settings[point_pos.index]["label"]
|
|
558
|
-
|
|
559
|
-
# Identify groups and calculate adjustments
|
|
560
|
-
groups = grouper.identify_groups(point_positions)
|
|
561
|
-
adjustments = adjuster.calculate_adjustments(groups, len(settings))
|
|
562
|
-
|
|
563
|
-
# Draw each point
|
|
564
|
-
output_parts = []
|
|
565
|
-
|
|
566
|
-
for position_idx, abs_position in enumerate(sorted_positions):
|
|
567
|
-
point_idx = position_map[abs_position]
|
|
568
|
-
|
|
569
|
-
# Calculate positioning
|
|
570
|
-
point_radius = RadiusCalculator.get_point_radius(
|
|
571
|
-
point_idx, config.chart_type, bool(position_idx % 2)
|
|
572
|
-
)
|
|
573
|
-
|
|
574
|
-
adjusted_offset = renderer.calculate_offset(
|
|
575
|
-
abs_positions[point_idx], adjustments[position_idx]
|
|
576
|
-
)
|
|
577
|
-
|
|
578
|
-
# Calculate coordinates
|
|
579
|
-
point_x = sliceToX(0, config.radius - point_radius, adjusted_offset) + point_radius
|
|
580
|
-
point_y = sliceToY(0, config.radius - point_radius, adjusted_offset) + point_radius
|
|
581
|
-
|
|
582
|
-
# Draw external natal lines if needed
|
|
583
|
-
if config.chart_type == "ExternalNatal":
|
|
584
|
-
true_offset = renderer.calculate_offset(abs_positions[point_idx])
|
|
585
|
-
line_svg = renderer.draw_external_natal_lines(
|
|
586
|
-
point_radius, true_offset, adjusted_offset, settings[point_idx]["color"]
|
|
587
|
-
)
|
|
588
|
-
output_parts.append(line_svg)
|
|
589
|
-
|
|
590
|
-
# Generate point SVG
|
|
591
|
-
point_svg = renderer.generate_point_svg(
|
|
592
|
-
celestial_points[point_idx], point_x, point_y, settings[point_idx]["name"]
|
|
593
|
-
)
|
|
594
|
-
output_parts.append(point_svg)
|
|
595
|
-
|
|
596
|
-
return ''.join(output_parts)
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
def draw_planets_v2(
|
|
600
|
-
radius: Union[int, float],
|
|
601
|
-
available_kerykeion_celestial_points: List[KerykeionPointModel],
|
|
602
|
-
available_planets_setting: List[KerykeionSettingsCelestialPointModel],
|
|
603
|
-
third_circle_radius: Union[int, float],
|
|
604
|
-
main_subject_first_house_degree_ut: Union[int, float],
|
|
605
|
-
main_subject_seventh_house_degree_ut: Union[int, float],
|
|
606
|
-
chart_type: ChartType,
|
|
607
|
-
second_subject_available_kerykeion_celestial_points: Optional[List[KerykeionPointModel]] = None,
|
|
608
|
-
) -> str:
|
|
609
|
-
"""
|
|
610
|
-
Draw celestial points on an astrological chart.
|
|
611
|
-
|
|
612
|
-
This is the main entry point for drawing planets and other celestial points
|
|
613
|
-
on astrological charts. It handles positioning, grouping, and overlap resolution.
|
|
614
|
-
|
|
615
|
-
Args:
|
|
616
|
-
radius: Chart radius in pixels
|
|
617
|
-
available_kerykeion_celestial_points: Main subject's celestial points
|
|
618
|
-
available_planets_setting: Settings for celestial points
|
|
619
|
-
third_circle_radius: Radius of the third circle
|
|
620
|
-
main_subject_first_house_degree_ut: First house degree
|
|
621
|
-
main_subject_seventh_house_degree_ut: Seventh house degree
|
|
622
|
-
chart_type: Type of chart being drawn
|
|
623
|
-
second_subject_available_kerykeion_celestial_points: Secondary subject's points
|
|
624
|
-
|
|
625
|
-
Returns:
|
|
626
|
-
SVG string for the celestial points
|
|
627
|
-
|
|
628
|
-
Raises:
|
|
629
|
-
KerykeionException: If secondary points are required but not provided
|
|
630
|
-
"""
|
|
631
|
-
# Validate inputs
|
|
632
|
-
_validate_chart_inputs(chart_type, second_subject_available_kerykeion_celestial_points)
|
|
633
|
-
|
|
634
|
-
# Create configuration
|
|
635
|
-
config = ChartConfiguration(
|
|
636
|
-
radius=float(radius),
|
|
637
|
-
third_circle_radius=float(third_circle_radius),
|
|
638
|
-
first_house_degree=float(main_subject_first_house_degree_ut),
|
|
639
|
-
seventh_house_degree=float(main_subject_seventh_house_degree_ut),
|
|
640
|
-
chart_type=chart_type
|
|
641
|
-
)
|
|
642
|
-
|
|
643
|
-
# Initialize components
|
|
644
|
-
grouper = CelestialPointGrouper()
|
|
645
|
-
adjuster = PositionAdjuster()
|
|
646
|
-
renderer = SVGRenderer(config)
|
|
647
|
-
|
|
648
|
-
# Process main celestial points
|
|
649
|
-
output_parts = []
|
|
650
|
-
|
|
651
|
-
if available_kerykeion_celestial_points:
|
|
652
|
-
main_svg = _draw_main_points(
|
|
653
|
-
available_kerykeion_celestial_points,
|
|
654
|
-
available_planets_setting,
|
|
655
|
-
config,
|
|
656
|
-
grouper,
|
|
657
|
-
adjuster,
|
|
658
|
-
renderer
|
|
659
|
-
)
|
|
660
|
-
output_parts.append(main_svg)
|
|
661
|
-
|
|
662
|
-
# Process secondary points if needed
|
|
663
|
-
if _requires_secondary_points(chart_type) and second_subject_available_kerykeion_celestial_points:
|
|
664
|
-
secondary_renderer = SecondaryPointsRenderer(config)
|
|
665
|
-
|
|
666
|
-
secondary_abs_positions = [p.abs_pos for p in second_subject_available_kerykeion_celestial_points]
|
|
667
|
-
secondary_rel_positions = [p.position for p in second_subject_available_kerykeion_celestial_points]
|
|
668
|
-
exclude_points = list(get_args(Houses)) if chart_type == "Transit" else []
|
|
669
|
-
|
|
670
|
-
secondary_svg = secondary_renderer.draw_secondary_points(
|
|
671
|
-
secondary_abs_positions,
|
|
672
|
-
secondary_rel_positions,
|
|
673
|
-
available_planets_setting,
|
|
674
|
-
exclude_points
|
|
675
|
-
)
|
|
676
|
-
output_parts.append(secondary_svg)
|
|
677
|
-
|
|
678
|
-
return ''.join(output_parts)
|
|
679
|
-
|