-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathfixstarsparallelswnd.py
More file actions
636 lines (541 loc) · 27.1 KB
/
fixstarsparallelswnd.py
File metadata and controls
636 lines (541 loc) · 27.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
# -*- coding: utf-8 -*-
import math
import wx
import os
import astrology
import planets
import fixstars
import fixstardirs
import fortune
import houses
import chart
import common
import commonwnd
import Image, ImageDraw, ImageFont
import mtexts
import util
import re
ARCSEC = 3600.0
def _normkey(s):
try:
return re.sub(r'[^0-9A-Za-z]+', '', s).upper()
except Exception:
return (s or '').upper()
def _absdeg(x): return abs(x)
def _degdiff(a, b): return abs(a - b)
class FixStarsParallelsWnd(commonwnd.CommonWnd):
"""
Tables > Fixed Stars > Fixed Star Parallels
Declination Parallels between angles/planets/points and selected fixed stars.
"""
def __init__(self, parent, chrt, options, mainfr, id=-1, size=wx.DefaultSize):
commonwnd.CommonWnd.__init__(self, parent, chrt, options, id, size)
self.mainfr = mainfr
# === sizes: follow positionswnd ===
self.FONT_SIZE = int(21 * self.options.tablesize)
self.SPACE = self.FONT_SIZE // 2
self.LINE_HEIGHT = self.SPACE + self.FONT_SIZE + self.SPACE
self.SMALL_CELL_WIDTH = 3 * self.FONT_SIZE # symbol/point
self.CELL_WIDTH = 8 * self.FONT_SIZE # decl columns
self.BIG_CELL_WIDTH = 10 * self.FONT_SIZE # star name (as fixstarswnd)
self.COLUMN_NUM = 4 # (blank/point) | decl_pt | star_name | decl_star
BOR = commonwnd.CommonWnd.BORDER
self.TABLE_WIDTH = self.SMALL_CELL_WIDTH + self.CELL_WIDTH + self.BIG_CELL_WIDTH + self.CELL_WIDTH
header_h = self.LINE_HEIGHT
body_rows = self._row_count()
self.TABLE_HEIGHT = header_h + body_rows * self.LINE_HEIGHT
self.WIDTH = int(2 * BOR + self.TABLE_WIDTH)
self.HEIGHT = int(2 * BOR + self.TABLE_HEIGHT)
self.SetBackgroundColour(self.options.clrbackground)
self.SetVirtualSize((self.WIDTH, self.HEIGHT))
# fonts: follow positionswnd/fixstarswnd
self.fntMorinus = ImageFont.truetype(common.common.symbols, int(self.FONT_SIZE))
self.fntSymbol = ImageFont.truetype(common.common.symbols, int(3 * self.FONT_SIZE / 2))
self.fntText = ImageFont.truetype(common.common.abc, int(self.FONT_SIZE))
self.fntTextBold = ImageFont.truetype(common.common.abc_bold, int(self.FONT_SIZE)) if os.path.exists(common.common.abc_bold) else self.fntText
self.deg_symbol = u'\u00b0'
# dignity 팔레트 (positionswnd와 동일)
self.clrs = (self.options.clrdomicil, self.options.clrexal,
self.options.clrperegrin, self.options.clrcasus, self.options.clrexil)
# glyphs (angles) same codes as fixstarsaspectswnd
self.GLY_ANGLE = {'ASC': u'0', 'MC': u'1', 'IC': u'2', 'DSC': u'3'}
# Node glyphs: use the same shared symbols as other windows
# Node glyphs: use shared symbols; fallback to unicode then draw with text font
self.NN_CHAR = getattr(common.common, 'nnode', None)
self.SN_CHAR = getattr(common.common, 'snode', None)
# 보조 시도: Planets 맵의 확장 인덱스 사용(환경에 따라 존재)
if not self.NN_CHAR:
try:
self.NN_CHAR = common.common.Planets[astrology.SE_PLUTO+1]
except Exception:
pass
if not self.SN_CHAR:
try:
self.SN_CHAR = common.common.Planets[astrology.SE_PLUTO+2]
except Exception:
pass
# 최종 폴백: 유니코드
if not self.NN_CHAR: self.NN_CHAR = u'☊'
if not self.SN_CHAR: self.SN_CHAR = u'☋'
# 유니코드 폴백이면 심볼 폰트 대신 텍스트 폰트 사용
self._node_use_text_font = (self.NN_CHAR in (u'☊',) or self.SN_CHAR in (u'☋',))
self.LOF_CHAR = u'4'
# color setup
self.tableclr = (0,0,0) if self.bw else self.options.clrtable
self.textclr = (0,0,0) if self.bw else self.options.clrtexts
# sefstars.txt에서 밝기 인덱스 우선 로딩(실패해도 무시)
self._sef_mag_index = {}
try:
self._sef_mag_index = self._build_sefstars_mag_index()
except Exception:
self._sef_mag_index = {}
# precompute star magnitudes map by code (lazy on demand)
self._mag_cache = {}
# precompute decls of points
self._points = self._collect_points()
self.buffer = None
self.drawBkg()
# ---- CommonWnd hook for Save As Bitmap file name suffix ----
def getExt(self):
# 파일명 꼬리표(예: John_Doe_FixStarParallels.bmp). mtexts 키 없이도 안전하게 동작
return u'_FixStarParallels'
# ---------- geometry helpers ----------
def _total_row_count(self):
# 각 포인트마다: 매치가 0이면 1행, n개면 n행
total = 0
for key in self._point_order():
pdecl = self._points.get(key, None)
matches = self._compute_matches(pdecl)
total += max(1, len(matches))
return total
def _row_count(self):
# header + each point as one row with possibly multiple star matches -> we draw point once then matched stars stacked
# For height estimate we conservatively allocate one line per point; drawing handles multi-matches by stacking extra lines.
# To avoid dynamic resize flicker, keep this simple: allocate Npoints * max(1, avg matches). We'll stick to 1; wx scroll handles overflow.
return len(self._point_order())
def _build_sefstars_mag_index(self):
"""
sefstars.txt에서 [표준명 → mag] 맵을 구축.
위치는 여러 후보 경로를 순차적으로 탐색.
"""
idx = {}
candidates = [
os.path.join(os.getcwd(), 'sefstars.txt'),
os.path.join(os.path.dirname(__file__), 'sefstars.txt'),
os.path.join(os.path.dirname(getattr(astrology, '__file__', __file__)), 'sefstars.txt'),
os.path.join('se', 'sefstars.txt'),
os.path.join('data', 'sefstars.txt'),
]
path = None
for p in candidates:
try:
if os.path.exists(p):
path = p
break
except Exception:
pass
if not path:
return idx
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
for line in f:
s = line.strip()
if not s or s[:1] in ('#', ';', '*'):
continue
# 세미콜론/탭 우선 분리, 없으면 연속 공백 분리
parts = [t.strip() for t in re.split(r'[;\t]+', s) if t.strip()]
if not parts:
continue
# 첫 필드를 노미널 코드로 간주(대소문자 무시 키)
nom = parts[0].strip()
# 나머지에서 '소수점 포함'하고 -3~10 사이 값 중 하나를 mag로 채택
# (RA/Dec 소수와 충돌 가능성은 작게, 그래도 실패하면 스킵)
mag = None
rest = ' '.join(parts[1:])
for tok in re.split(r'[\s,]+', rest):
try:
if '.' in tok:
v = float(tok)
if -3.0 <= v <= 10.0:
mag = v
break
except Exception:
pass
if mag is not None:
idx[nom.upper()] = mag
return idx
def _point_order(self):
# Angles first, then planets, nodes, fortuna
return ['ASC','DSC','MC','IC',
astrology.SE_SUN, astrology.SE_MOON, astrology.SE_MERCURY, astrology.SE_VENUS,
astrology.SE_MARS, astrology.SE_JUPITER, astrology.SE_SATURN,
astrology.SE_URANUS, astrology.SE_NEPTUNE, astrology.SE_PLUTO,
'NN', 'SN', 'LOF']
# ---------- data collection ----------
def _collect_points(self):
pts = {}
obl = self.chart.obl[0]
# Angles decl from ascmc2 (ASC/MC), others compute
asc_lon = self.chart.houses.ascmc2[houses.Houses.ASC][houses.Houses.LON]
asc_decl = self.chart.houses.ascmc2[houses.Houses.ASC][houses.Houses.DECL]
mc_lon = self.chart.houses.ascmc2[houses.Houses.MC][houses.Houses.LON]
mc_decl = self.chart.houses.ascmc2[houses.Houses.MC][houses.Houses.DECL]
# Dsc/IC decl from ecliptic lon+180, lat=0
dsc_ra, dsc_decl, _ = astrology.swe_cotrans(util.normalize(asc_lon + 180.0), 0.0, 1.0, -obl)
ic_ra, ic_decl, _ = astrology.swe_cotrans(util.normalize(mc_lon + 180.0), 0.0, 1.0, -obl)
pts['ASC'] = asc_decl
pts['DSC'] = dsc_decl
pts['MC'] = mc_decl
pts['IC'] = ic_decl
# Planets decl (Swiss Ephem already computed in chart.planets)
for p in (astrology.SE_SUN, astrology.SE_MOON, astrology.SE_MERCURY, astrology.SE_VENUS,
astrology.SE_MARS, astrology.SE_JUPITER, astrology.SE_SATURN,
astrology.SE_URANUS, astrology.SE_NEPTUNE, astrology.SE_PLUTO):
pts[p] = self.chart.planets.planets[p].dataEqu[planets.Planet.DECLEQU]
# Nodes: mean/true node in chart as SE node; SN opposite
node_decl = self.chart.planets.planets[astrology.SE_PLUTO+1].dataEqu[planets.Planet.DECLEQU] # node appended after pluto
# South Node is exact opposite in ecliptic longitude but decl is ~(-) of NN for small lat → 여기선 관례적으로 부호 반전 사용
pts['NN'] = node_decl
pts['SN'] = -node_decl
# Fortuna decl
# 옵션의 Lot-of-Fortune 타입을 morinus 내부 상수로 변환
lot_idx = getattr(self.options, 'lotoffortune', 2)
if lot_idx == 0: lftype = chart.Chart.LFMOONSUN
elif lot_idx == 1: lftype = chart.Chart.LFDSUNMOON
else: lftype = chart.Chart.LFDMOONSUN
# 태양 고도(ELV)로 주간/야간 판정 → abovehorizon
sun_elv = self.chart.planets.planets[astrology.SE_SUN].speculums[1][planets.Planet.ELV]
aboveh = (sun_elv > 0.0)
# 황도 경사 값 안전 취득
obl_val = self.chart.obl[0] if isinstance(self.chart.obl, (list, tuple)) else self.chart.obl
# Fortune(typ, ascmc2, raequasc, pls, obl, placelat, abovehorizon)
fort = fortune.Fortune(
lftype,
self.chart.houses.ascmc2,
self.chart.raequasc,
self.chart.planets,
obl_val,
self.chart.place.lat,
aboveh
)
pts['LOF'] = fort.fortune[fortune.Fortune.DECL]
return pts
def _star_mag_from_sef_or_fallback(self, code, fallback_name=None):
"""
0) Swiss Ephemeris swe_fixstar_mag() 결과를 우선 사용
1) 실패 시 sefstars.txt 인덱스 조회(코드/표시명 정규화 키 모두 시도)
2) 그래도 실패하면 기존 카탈로그 lookup으로 폴백
"""
key = (code or fallback_name or '').strip()
if not key:
return None
# 캐시
cache_key = ('SEF', key)
if cache_key in self._mag_cache:
return self._mag_cache[cache_key]
mag = None
# 0) Swiss Ephemeris swe_fixstar_mag 우선
ids = []
for s in (code, fallback_name):
if not s:
continue
try:
t = u"%s" % (s,)
except Exception:
t = s
try:
t = t.replace(u'\x00', u'').strip()
except Exception:
t = u"%s" % (t,)
t = t.strip()
if t and t not in ids:
ids.append(t)
for sid in ids:
query = sid
try:
ret, st, mag_val, serr = astrology.swe_fixstar_mag(',' + query)
except Exception:
mag_val = None
cand_mag = None
if mag_val is not None:
try:
# mag_val 이 (2.75,) 같은 튜플이므로 첫 원소를 사용
if isinstance(mag_val, (list, tuple)):
raw = mag_val[0] if len(mag_val) > 0 else None
else:
raw = mag_val
if raw is not None:
cand_mag = float(raw)
except Exception:
cand_mag = None
# 고정별 겉보기 등급의 일반적인 범위(-5~+15 정도) 안에서만 채택
if cand_mag is not None and -15.0 < cand_mag < 20.0 and cand_mag < 998.0:
mag = cand_mag
if mag is not None:
break
# 1) sefstars.txt 인덱스 조회
if mag is None and self._sef_mag_index:
m = self._sef_mag_index.get((code or '').upper())
if m is None and fallback_name:
m = self._sef_mag_index.get((fallback_name or '').upper())
# 이름 정규화 키로도 한 번 더 시도
if m is None:
nk = _normkey(code or '')
m = self._sef_mag_index.get(nk)
if m is None and fallback_name:
nk = _normkey(fallback_name or '')
m = self._sef_mag_index.get(nk)
mag = m
# 2) 폴백: 기존 generic lookup
if mag is None:
try:
_ra, _dc, mag, _frame = fixstars._cat_lookup_equ_generic(code or fallback_name or '')
except Exception:
try:
from fixstardirs import _cat_lookup_equ_generic as _cat
_ra, _dc, mag, _frame = _cat(code or fallback_name or '')
except Exception:
mag = None
self._mag_cache[cache_key] = mag
return mag
# ---------- magnitude & bold threshold ----------
def _star_mag(self, code, fallback_name=None):
key = code or (fallback_name or '')
if key in self._mag_cache:
return self._mag_cache[key]
mag = None
try:
_ra, _dc, mag, _frame = fixstardirs._cat_lookup_equ_generic(code or fallback_name or '')
except Exception:
pass
self._mag_cache[key] = mag
return mag
def _bold_orb_deg(self, mag):
# minutes → degrees
if mag is None:
return 5.0/60.0 # 보수적(가장 엄격)
if mag < 0.0:
return 10.0/60.0
if 0.0 <= mag < 1.0:
return 8.0/60.0
if 1.0 <= mag <= 1.5:
return 6.0/60.0
return 5.0/60.0
def drawBkg(self):
# CommonWnd가 호출하는 재그리기 엔트리. 흑백 토글 후에도 여기로 들어옴.
self.Draw()
# ---------- drawing ----------
def Draw(self):
BOR = commonwnd.CommonWnd.BORDER
# (추가) 동적 높이 계산: 추가 매치로 생기는 행까지 반영
total_rows = self._total_row_count()
header_h = self.LINE_HEIGHT
self.TABLE_HEIGHT = header_h + total_rows * self.LINE_HEIGHT
self.HEIGHT = int(2 * BOR + self.TABLE_HEIGHT)
self.SetVirtualSize((self.WIDTH, self.HEIGHT))
img = Image.new('RGB', (int(self.WIDTH), int(self.HEIGHT)), (255,255,255) if self.bw else self.options.clrbackground)
draw = ImageDraw.Draw(img)
# header (no left title)
x = BOR; y = BOR
self._draw_header(draw, x, y)
y += self.LINE_HEIGHT
# rows
for key in self._point_order():
y = self._draw_point_row(draw, x, y, key)
wxImg = wx.Image(img.size[0], img.size[1])
wxImg.SetData(img.tobytes())
self.buffer = wx.Bitmap(wxImg)
self.Refresh(False)
def _draw_header(self, draw, x, y):
# top border intentionally omitted (positionswnd style)
txtclr = (0,0,0) if self.bw else self.options.clrtexts
offs = (0, self.SMALL_CELL_WIDTH, self.CELL_WIDTH, self.BIG_CELL_WIDTH, self.CELL_WIDTH)
# 헤더 세로선:
# - 왼쪽 Declination의 '왼쪽' 경계선만 유지
draw.line((x + self.SMALL_CELL_WIDTH, y, x + self.SMALL_CELL_WIDTH, y + self.LINE_HEIGHT), fill=self.tableclr)
# - Fixed Star 양쪽 세로 구분선은 그리지 않음
# - 오른쪽 Declination의 '오른쪽'(표의 맨 오른쪽) 테두리 추가
draw.line((x + self.TABLE_WIDTH, y, x + self.TABLE_WIDTH, y + self.LINE_HEIGHT), fill=self.tableclr)
# 헤더 하단선: 표 전체 폭 유지
draw.line((x, y + self.LINE_HEIGHT, x + self.TABLE_WIDTH, y + self.LINE_HEIGHT), fill=self.tableclr)
# 헤더 상단선: Asc 칸은 제외 → 왼쪽 Declination 시작점부터 오른쪽 테두리까지
draw.line((x + self.SMALL_CELL_WIDTH, y, x + self.TABLE_WIDTH, y), fill=self.tableclr)
# captions
# captions (localized)
cap_decl = mtexts.txts['Declination'] # exists in all langs
cap_star = mtexts.txts.get('FixedStar', mtexts.txts.get('FixedStar','Fixed Star')) # fallback to plural or EN
captions = ('', cap_decl, cap_star, cap_decl)
# locale: keep English minimal; advanced localization unnecessary here
for idx, cap in enumerate(captions):
if not cap:
continue
colx = x + sum(offs[:idx+1])
w,h = draw.textsize(cap, self.fntText)
draw.text((colx + (offs[idx+1]-w)/2, y + (self.LINE_HEIGHT-h)/2), cap, fill=txtclr, font=self.fntText)
def _draw_point_row(self, draw, x, y, point_key):
# cell grid bottom line
draw.line((x, y+self.LINE_HEIGHT, x+self.TABLE_WIDTH, y+self.LINE_HEIGHT), fill=self.tableclr)
offs = (0, self.SMALL_CELL_WIDTH, self.CELL_WIDTH, self.BIG_CELL_WIDTH, self.CELL_WIDTH)
for i in range(1, len(offs)-1):
draw.line((x+sum(offs[:i+1]), y, x+sum(offs[:i+1]), y+self.LINE_HEIGHT), fill=self.tableclr)
# left/right outer borders for the row
draw.line((x, y, x, y+self.LINE_HEIGHT), fill=self.tableclr)
draw.line((x+self.TABLE_WIDTH, y, x+self.TABLE_WIDTH, y+self.LINE_HEIGHT), fill=self.tableclr)
txtclr = (0,0,0) if self.bw else self.options.clrtexts
# 1) left cell: symbol/name (행성/각/노드/LoF는 심볼폰트 1.5×, N/S는 텍스트)
sym = self._point_symbol(point_key)
if (isinstance(point_key, int) and astrology.SE_SUN <= point_key <= astrology.SE_PLUTO) \
or point_key in ('ASC','DSC','MC','IC','NN','SN','LOF'):
fontL = self.fntSymbol
else:
fontL = self.fntText
iconclr = self._label_color(point_key)
w, h = draw.textsize(sym, fontL)
draw.text((x + (self.SMALL_CELL_WIDTH - w)//2, y + (self.LINE_HEIGHT - h)//2),
sym, fill=iconclr, font=fontL)
# 2) point decl (first decl column) + "행(매치)별" 볼드 판단
pdecl = self._points.get(point_key, None)
matches = self._compute_matches(pdecl)
def _is_bold_row(p, s, m):
return (p is not None and s is not None and (p * s) >= 0.0 and (_degdiff(p, s) <= self._bold_orb_deg(m)))
txt_decl_pt = self._fmt_decl(pdecl)
# 첫 번째 매치 기준으로 1행째 볼드 여부 결정
row_bold_first = _is_bold_row(pdecl, matches[0][2], matches[0][3]) if matches else False
font_decl_pt_first = self.fntTextBold if row_bold_first else self.fntText
w,h = draw.textsize(txt_decl_pt, font_decl_pt_first)
draw.text((x + offs[1] + (self.CELL_WIDTH - w)/2, y + (self.LINE_HEIGHT - h)/2),
txt_decl_pt, fill=txtclr, font=font_decl_pt_first)
if matches:
first, rest = matches[0], matches[1:]
# 첫 행: 첫 매치의 볼드 여부로 렌더
self._draw_star_cols(draw, x, y, first, pdecl, row_bold_first)
# stack others
y0 = y
for m in rest:
y0 += self.LINE_HEIGHT
# bottom border for stacked line
draw.line((x, y0+self.LINE_HEIGHT, x+self.TABLE_WIDTH, y0+self.LINE_HEIGHT), fill=self.tableclr)
for i in range(1, len(offs)-1):
draw.line((x+sum(offs[:i+1]), y0, x+sum(offs[:i+1]), y0+self.LINE_HEIGHT), fill=self.tableclr)
# left/right outer borders for the stacked line
draw.line((x, y0, x, y0+self.LINE_HEIGHT), fill=self.tableclr)
draw.line((x+self.TABLE_WIDTH, y0, x+self.TABLE_WIDTH, y0+self.LINE_HEIGHT), fill=self.tableclr)
# (추가) 왼쪽 심볼 반복 표시
wL, hL = draw.textsize(sym, fontL)
draw.text((x + (self.SMALL_CELL_WIDTH - wL)//2, y0 + (self.LINE_HEIGHT - hL)//2),
sym, fill=iconclr, font=fontL)
row_bold_m = _is_bold_row(pdecl, m[2], m[3])
font_decl_pt_m = self.fntTextBold if row_bold_m else self.fntText
# (추가) 포인트 적위 반복 표시(행 볼드 규칙 적용)
wD, hD = draw.textsize(txt_decl_pt, font_decl_pt_m)
draw.text((x + offs[1] + (self.CELL_WIDTH - wD)/2, y0 + (self.LINE_HEIGHT - hD)/2),
txt_decl_pt, fill=txtclr, font=font_decl_pt_m)
# 항성 이름/적위
self._draw_star_cols(draw, x, y0, m, pdecl, row_bold_m)
return y0 + self.LINE_HEIGHT
else:
# 매치가 하나도 없으면 두 칸 모두 '—' 출력
dash = u'—'
# Fixed Star 칸
colx = x + sum(offs[:3])
w,h = draw.textsize(dash, self.fntText)
draw.text((colx + (offs[3]-w)/2, y + (self.LINE_HEIGHT - h)/2), dash, fill=txtclr, font=self.fntText)
# 오른쪽 Declination 칸
w,h = draw.textsize(dash, self.fntText)
draw.text((x + sum(offs[:4]) + (self.CELL_WIDTH - w)/2, y + (self.LINE_HEIGHT - h)/2),
dash, fill=txtclr, font=self.fntText)
return y + self.LINE_HEIGHT
def _draw_star_cols(self, draw, x, y, match, pdecl, row_bold):
txtclr = (0,0,0) if self.bw else self.options.clrtexts
code, dispname, sdecl, mag = match
# 행 전체 볼드: 조건 충족 시 이름/적위 모두 Bold (행성 기호는 이미 별도로 비볼드)
font_nm = self.fntTextBold if row_bold else self.fntText
font_decl = font_nm
offs = (0, self.SMALL_CELL_WIDTH, self.CELL_WIDTH, self.BIG_CELL_WIDTH, self.CELL_WIDTH)
# 이름
colx = x + sum(offs[:3])
w,h = draw.textsize(dispname, font_nm)
draw.text((colx + (offs[3]-w)/2, y + (self.LINE_HEIGHT-h)/2), dispname, fill=txtclr, font=font_nm)
# 항성 적위
txt_decl_star = self._fmt_decl(sdecl)
w,h = draw.textsize(txt_decl_star, font_decl)
draw.text((x + sum(offs[:4]) + (self.CELL_WIDTH-w)/2, y + (self.LINE_HEIGHT-h)/2),
txt_decl_star, fill=txtclr, font=font_decl)
# ---------- computations ----------
def _compute_matches(self, pdecl):
res = []
if pdecl is None:
return res
for idx in range(len(self.chart.fixstars.data)):
code = self.chart.fixstars.data[idx][fixstars.FixStars.NOMNAME]
name_sw = self.chart.fixstars.data[idx][fixstars.FixStars.NAME] or code
disp = astrology.display_fixstar_name(code, self.options, name_sw)
sdecl = self.chart.fixstars.data[idx][fixstars.FixStars.DECL]
# sign equal & within 15'
if pdecl * sdecl < 0:
continue
if _degdiff(pdecl, sdecl) <= (15.0/60.0):
mag = self._star_mag_from_sef_or_fallback(code, name_sw)
res.append((code, disp, sdecl, mag))
# keep sorted by absolute orb
res.sort(key=lambda x: _degdiff(pdecl, x[2]))
return res
# ---------- presentation helpers ----------
def _fmt_decl(self, val):
if val is None:
return u'—'
d, m, s = util.decToDeg(abs(val) + 1e-8)
sign = '-' if val < 0.0 else ''
# positionswnd 규칙: 부호는 음수만 '-', 도·분·초 모두 표시, 공백 없음, 분/초 기호 ' / "
return sign + (str(int(d))).rjust(2) + u'\u00b0' + (str(int(m))).zfill(2) + "'" + (str(int(s))).zfill(2) + '"'
def _label_color(self, key):
# BW면 무조건 검정
if self.bw:
return (0, 0, 0)
# 행성: 개인색 사용 or 존위색(dignity)
if isinstance(key, int) and astrology.SE_SUN <= key <= astrology.SE_PLUTO:
if self.options.useplanetcolors:
return self.options.clrindividual[key]
dign = self.chart.dignity(key)
return self.clrs[dign]
# 노드(N/S) 개인색
if key in ('NN', 'SN'):
return self.options.clrindividual[astrology.SE_PLUTO+1] if self.options.useplanetcolors else self.options.clrtexts
# 포르투나(LoF) 개인색
if key == 'LOF':
return self.options.clrindividual[astrology.SE_PLUTO+2] if self.options.useplanetcolors else self.options.clrtexts
# 각도(ASC/DSC/MC/IC)나 기타 기본
return self.options.clrtexts
def _is_symbol(self, s):
try:
return isinstance(s, str) and len(s) == 1
except Exception:
return False
def _point_symbol(self, key):
# angles
if key == 'ASC': return self.GLY_ANGLE['ASC']
if key == 'DSC': return self.GLY_ANGLE['DSC']
if key == 'MC' : return self.GLY_ANGLE['MC']
if key == 'IC' : return self.GLY_ANGLE['IC']
# Fortuna glyph (as positionswnd)
# Fortuna
if key == 'LOF':
return self.LOF_CHAR
# Nodes (from fixstarsaspectswnd or fallback set in __init__)
if key == 'NN':
return self.NN_CHAR
if key == 'SN':
return self.SN_CHAR
# Planets: use the configured symbol map (user setting respected; incl. Uranus/Pluto)
if isinstance(key, int) and astrology.SE_SUN <= key <= astrology.SE_PLUTO:
try:
return common.common.Planets[key]
except Exception:
# 안전 폴백(환경 독립)
fallback = {
astrology.SE_SUN: u'A', astrology.SE_MOON: u'B', astrology.SE_MERCURY: u'C',
astrology.SE_VENUS: u'D',astrology.SE_MARS: u'E', astrology.SE_JUPITER: u'F',
astrology.SE_SATURN: u'G',astrology.SE_URANUS: u'H',astrology.SE_NEPTUNE: u'I',
astrology.SE_PLUTO: u'J',
}
return fallback.get(key, u'?')
return u'?'