'use strict';
// ===== DOM Refs =====
const form = document.getElementById('chart-form');
const dateInput = document.getElementById('input-date');
const timeInput = document.getElementById('input-time');
const tzSelect = document.getElementById('input-tz');
const latInput = document.getElementById('input-lat');
const lonInput = document.getElementById('input-lon');
const nameInput = document.getElementById('input-name');
const houseSelect = document.getElementById('input-house');
const calcBtn = document.getElementById('calc-btn');
const locateBtn = document.getElementById('locate-btn');
const locateStatus = document.getElementById('locate-status');
const resultsPanel = document.getElementById('results-panel');
const loadingOverlay = document.getElementById('loading-overlay');
// ===== State =====
let chartData = null;
let map = null;
let marker = null;
// ===== Init Date/Time to Now =====
function initDateTime() {
const now = new Date();
const yyyy = now.getFullYear();
const mm = String(now.getMonth() + 1).padStart(2, '0');
const dd = String(now.getDate()).padStart(2, '0');
const hh = String(now.getHours()).padStart(2, '0');
const mi = String(now.getMinutes()).padStart(2, '0');
const ss = String(now.getSeconds()).padStart(2, '0');
dateInput.value = `${yyyy}-${mm}-${dd}`;
timeInput.value = `${hh}:${mi}:${ss}`;
}
// ===== Geolocation =====
function locateUser() {
if (!navigator.geolocation) {
locateStatus.textContent = '浏览器不支持定位';
return;
}
locateBtn.classList.add('locating');
locateBtn.textContent = '📍 定位中...';
locateStatus.textContent = '';
navigator.geolocation.getCurrentPosition(
function(pos) {
const lat = pos.coords.latitude.toFixed(4);
const lon = pos.coords.longitude.toFixed(4);
latInput.value = lat;
lonInput.value = lon;
// Update map marker
if (marker && map) {
marker.setLatLng([lat, lon]);
map.setView([lat, lon], 12);
}
// Auto-detect timezone
const tzOffset = -pos.coords.longitude / 15; // rough estimate
const tzRounded = Math.round(tzOffset * 4) / 4; // round to nearest 0.25h
const tzStr = tzRounded >= 0
? `+${tzRounded}`.replace('.25',':15').replace('.5',':30').replace('.75',':45')
: `-${Math.abs(tzRounded)}`.replace('.25',':15').replace('.5',':30').replace('.75',':45');
// Try to find closest match in timezone dropdown
for (const opt of tzSelect.options) {
if (opt.value === tzStr) {
tzSelect.value = tzStr;
break;
}
}
locateBtn.classList.remove('locating');
locateBtn.textContent = '📍 已定位';
locateStatus.textContent = `${lat}, ${lon}`;
setTimeout(() => { locateBtn.textContent = '📍 获取当前位置'; }, 2000);
},
function(err) {
locateBtn.classList.remove('locating');
locateBtn.textContent = '📍 定位失败';
locateStatus.textContent = '请手动输入或使用地图';
setTimeout(() => { locateBtn.textContent = '📍 获取当前位置'; }, 3000);
},
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 }
);
}
// ===== Planet names =====
const PLANET_CN = {
Su: '太阳', Mo: '月亮', Ma: '火星', Me: '水星',
Ju: '木星', Ve: '金星', Sa: '土星',
Ra: '罗睺', Ke: '计都', Ur: '天王', Ne: '海王', Pl: '冥王'
};
const PLANET_ORDER = ['Su','Mo','Ma','Me','Ju','Ve','Sa','Ra','Ke','Ur','Ne','Pl'];
// ===== Leaflet Map =====
function initMap() {
if (typeof L === 'undefined') return;
map = L.map('map', { zoomControl: true }).setView([39.907, 116.397], 4);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OSM',
maxZoom: 18,
}).addTo(map);
marker = L.marker([39.907, 116.397], { draggable: true }).addTo(map);
marker.on('dragend', updateCoordsFromMarker);
map.on('click', function(e) {
marker.setLatLng(e.latlng);
updateCoordsFromMarker();
});
}
function updateCoordsFromMarker() {
const pos = marker.getLatLng();
latInput.value = pos.lat.toFixed(4);
lonInput.value = pos.lng.toFixed(4);
}
// ===== Form Submit =====
form.addEventListener('submit', async function(e) {
e.preventDefault();
await calculateChart();
});
async function calculateChart() {
calcBtn.disabled = true;
calcBtn.textContent = '计算中...';
loadingOverlay.classList.add('visible');
try {
const payload = {
date: dateInput.value,
time: timeInput.value.length === 5 ? timeInput.value + ':00' : timeInput.value,
tz: tzSelect.value,
lat: latInput.value,
lon: lonInput.value,
name: nameInput.value || '',
house_system: houseSelect.value,
};
const resp = await fetch('/api/chart', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const result = await resp.json();
if (!result.success) {
alert('计算错误: ' + (result.error || '未知错误'));
return;
}
chartData = result.data;
resultsPanel.style.display = 'block';
renderAll();
} catch (err) {
alert('请求失败: ' + err.message);
} finally {
calcBtn.disabled = false;
calcBtn.textContent = '计算星盘';
loadingOverlay.classList.remove('visible');
}
}
// ===== Tabs =====
document.addEventListener('click', function(e) {
if (!e.target.classList.contains('tab')) return;
const tabGroup = e.target.parentElement;
const tabName = e.target.dataset.tab;
tabGroup.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
e.target.classList.add('active');
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
const panel = document.getElementById('tab-' + tabName);
if (panel) panel.classList.add('active');
});
// ===== Render All =====
function renderAll() {
if (!chartData) return;
renderPanchanga(chartData.panchanga);
renderPlanets(chartData.planets);
renderHouses(chartData.houses);
renderKarakas(chartData.karakas_7);
renderDashas(chartData.dasha_years, chartData.chara_dasha);
renderPadas(chartData.padas, chartData.upapada);
renderLagnas(chartData.special_lagnas, chartData.ascendant);
renderDivisions(chartData.divisions);
renderArgala(chartData.argalas, chartData.lagna_rajayoga, chartData.karakamsa_rajayoga);
}
// ===== Panchanga =====
function renderPanchanga(p) {
if (!p) return;
const container = document.getElementById('panchanga-cards');
container.innerHTML = [
renderTithiCard(p.tithi),
renderNakshatraCard(p.nakshatra),
renderYogaCard(p.yoga),
renderKaranaCard(p.karana),
renderVaraCard(p.vara),
].join('');
}
function renderTithiCard(t) {
if (!t) return '';
const pct = (t.progress * 100).toFixed(1);
const isShukla = t.paksha === 'Shukla';
const cls = isShukla ? 'progress-shukla' : 'progress-krishna';
const border = isShukla ? 'var(--accent-shukla)' : 'var(--accent-krishna)';
return `
${isShukla ? '☀️' : '🌙'}
Tithi 月日
${t.name}
${t.paksha_chinese} ${t.name_chinese}
第${t.number}月日 (${t.paksha_tithi}日)
${pct}%
`;
}
function renderNakshatraCard(n) {
if (!n) return '';
const pct = (n.progress * 100).toFixed(1);
return `
⭐
Nakshatra 星宿
${n.name}
${n.name_chinese} (${n.meaning})
Pada ${n.pada}/4 · Lord: ${n.lord}
${pct}%
`;
}
function renderYogaCard(y) {
if (!y) return '';
return `
🔗
Yoga 合朔
${y.name}
${y.name_chinese}
${y.meaning}
`;
}
function renderKaranaCard(k) {
if (!k) return '';
return `
🕐
Karana 半日
${k.name}
${k.name_chinese}
${k.type} · 第${k.half}半
`;
}
function renderVaraCard(v) {
if (!v) return '';
const icons = {0:'☀️',1:'🌙',2:'♂️',3:'☿',4:'♃',5:'♀',6:'♄'};
return `
${icons[v.index] || '📅'}
Vara 曜日
${v.name}
${v.name_chinese}
${v.planet_chinese}
`;
}
// ===== Planets =====
function renderPlanets(planets) {
if (!planets) return;
let html = `
| 行星 | 黄经 (°) | 星座 | 星内度数 | 速度 (°/d) | 方向 |
`;
for (const p of PLANET_ORDER) {
if (!planets[p]) continue;
const pl = planets[p];
html += `
| ${p} ${PLANET_CN[p]||''} |
${pl.lon.toFixed(4)} |
${pl.sign} |
${pl.sign_deg.toFixed(2)}° |
${(pl.speed||0).toFixed(4)} |
${pl.retrograde?'R':'D'} |
`;
}
document.getElementById('planet-table').innerHTML = html + '';
}
// ===== Houses =====
function renderHouses(houses) {
if (!houses) return;
let html = `
| 宫位 | 星座 | 宫头 | 星内度数 | 宫内行星 |
`;
for (const h of houses) {
const planets = h.planets || [];
const planetStr = planets.length ? planets.map(p => p + ' ' + (PLANET_CN[p]||'')).join(', ') : '—';
html += `
| H${h.house} |
${h.sign} |
${h.cusp.toFixed(4)}° |
${h.sign_deg.toFixed(2)}° |
${planetStr} |
`;
}
document.getElementById('house-table').innerHTML = html + '';
}
// ===== Karakas =====
function renderKarakas(karakas) {
if (!karakas) return;
let html = `
| 序号 | Karaka | 全名 | 行星 | 星座内度 | 星座 |
`;
for (const k of karakas) {
html += `
| ${k.rank} |
${k.karaka} |
${k.karaka_full} |
${k.planet} ${PLANET_CN[k.planet]||''} |
${k.degree_in_sign.toFixed(4)}° |
${k.sign} |
`;
}
document.getElementById('karaka-table').innerHTML = html + '';
}
// ===== Dashas =====
function renderDashas(dashaYears, charaDasha) {
// Dasha years grid
if (dashaYears) {
let grid = '';
for (const d of dashaYears) {
grid += `
${d.sign_name}
${d.years} 年
Lord: ${d.lord}
`;
}
document.getElementById('dasha-years-grid').innerHTML = grid;
}
// Chara Dasha table
if (charaDasha) {
let html = `
| 大运 | Lord | 年数 | Antar (子运) |
`;
for (const p of charaDasha) {
const antars = (p.antar||[]).map(a => `${a.sign_name}(${a.lord}:${a.years.toFixed(2)}y)`).join(', ');
html += `
| ${p.sign_name} |
${p.lord} |
${p.years.toFixed(2)} |
${antars} |
`;
}
document.getElementById('dasha-table').innerHTML = html + '';
}
}
// ===== Padas =====
function renderPadas(padas, upapada) {
if (!padas) return;
const order = ['1','2','3','4','5','6','7','8','9','10','11','12'];
let html = `
| 宫位 | Pada | Arudha | 星座 | Lord |
`;
for (const h of order) {
const p = padas[h];
if (!p) continue;
html += `
| H${h} |
${p.name||''} |
${p.sign_full||p.sign} |
${p.sign} |
${p.lord} |
`;
}
if (upapada) {
html += `
| H12 |
UL |
${upapada.sign_full} |
${upapada.sign} |
${upapada.lord} |
`;
}
document.getElementById('pada-table').innerHTML = html + '';
}
// ===== Special Lagnas =====
function renderLagnas(lagnas, asc) {
if (!lagnas) return;
let grid = '';
// Ascendant
if (asc) {
grid += `
Ascendant (上升)
${asc.sign} ${asc.sign_deg.toFixed(2)}°
${asc.sign_str}
`;
}
const order = ['HL', 'GL', 'VL'];
const names = {HL:'Hora Lagna', GL:'Ghatika Lagna', VL:'Varnada Lagna'};
for (const key of order) {
const l = lagnas[key];
if (!l || !l.sign_idx && l.sign_idx !== 0) {
grid += `
${key} — ${names[key]||key}
${typeof l === 'object' ? JSON.stringify(l) : l}
`;
continue;
}
grid += `
${key} — ${names[key]||key}
${l.sign}
Lord: ${l.lord||'—'}
`;
}
document.getElementById('lagnas-grid').innerHTML = grid;
}
// ===== Divisions =====
function renderDivisions(divisions) {
if (!divisions) return;
if (divisions.D9) {
let html = `| 行星 | D-9 Navamsa 星座 |
`;
for (const [name, data] of Object.entries(divisions.D9)) {
html += `| ${name} | ${data.sign} |
`;
}
document.getElementById('d9-table').innerHTML = html + '';
}
if (divisions.D3) {
let html = `| 行星 | D-3 Drekkana 星座 |
`;
for (const [name, data] of Object.entries(divisions.D3)) {
html += `| ${name} | ${data.sign} |
`;
}
document.getElementById('d3-table').innerHTML = html + '';
}
}
// ===== Argala =====
function renderArgala(argalas, lagnaRajayoga, karakamsaRajayoga) {
if (!argalas) return;
const order = ['1','2','3','4','5','6','7','8','9','10','11','12'];
let html = `
| 宫位 | Argala | Virodhargala | 净结果 |
`;
for (const h of order) {
const a = argalas[h];
if (!a) continue;
html += `
| H${h} |
${a.argala_count||0} |
${a.virodhargala_count||0} |
${a.net_result||'neutral'} |
`;
}
document.getElementById('argala-table').innerHTML = html + '';
// Rajayoga
if (lagnaRajayoga || karakamsaRajayoga) {
let grid = '';
if (lagnaRajayoga) {
grid += `
Lagna Argala Rajayoga
${lagnaRajayoga.type} (Level ${lagnaRajayoga.level})
${lagnaRajayoga.desc}
`;
}
if (karakamsaRajayoga) {
const kr = karakamsaRajayoga;
grid += `
Karakamsa Rajayoga
${kr.is_rajayoga?'YES':'No'} (Level ${kr.yoga_level})
AK: ${kr.ak_planet} · Karakamsa: ${kr.karakamsa_sign} · ${kr.description}
`;
}
document.getElementById('rajayoga-info').innerHTML = grid;
}
}
// ===== Export =====
function getExportFilename(ext) {
if (!chartData) return `jaimini-export.${ext}`;
const input = chartData.input || {};
const date = (input.date || 'unknown').replace(/-/g, '');
const name = (input.name || 'chart').replace(/[^a-zA-Z0-9一-鿿]/g, '_');
return `jaimini-${name}-${date}.${ext}`;
}
function downloadBlob(content, filename, mimeType) {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function exportJSON() {
if (!chartData) return;
// Add export timestamp
const payload = Object.assign({}, chartData);
payload.exported_at = new Date().toISOString();
const json = JSON.stringify(payload, null, 2);
downloadBlob(json, getExportFilename('json'), 'application/json');
}
function exportTXT() {
if (!chartData) return;
const d = chartData;
const algo = d.algorithm || {};
const inp = d.input || {};
const asc = d.ascendant || {};
const pcn = {Su:'太阳',Mo:'月亮',Ma:'火星',Me:'水星',Ju:'木星',Ve:'金星',Sa:'土星',Ra:'罗睺',Ke:'计都',Ur:'天王',Ne:'海王',Pl:'冥王'};
const signCN = {Ari:'白羊',Tau:'金牛',Gem:'双子',Cnc:'巨蟹',Leo:'狮子',Vir:'处女',Lib:'天秤',Sco:'天蝎',Sgr:'射手',Cap:'摩羯',Aqr:'水瓶',Psc:'双鱼'};
const lines = [];
const sep = '='.repeat(68);
const sub = '-'.repeat(62);
const ssub = '·'.repeat(50);
// ═══════════ HEADER ═══════════
lines.push(sep);
lines.push(' JAIMINI TROPICAL ASTROLOGY ENGINE — 完整星盘报告');
lines.push(` 导出时间: ${new Date().toLocaleString('zh-CN')}`);
lines.push(sep);
lines.push('');
// ═══════════ ALGORITHM ═══════════
lines.push('一、【算法标注 — 本章节内容是解读的前提,务必先行阅读】');
lines.push(sub);
const algoLines = [
['引擎名称', algo.engine || 'Jaimini Tropical Astrology Engine'],
['版本号', algo.version || '1.0.0'],
['占星体系', algo.tradition || ''],
['黄道系统', algo.zodiac || ''],
['宫位系统', algo.house_system || ''],
['行星历表', algo.ephemeris || ''],
['主星系统', algo.karaka_system || ''],
['大运系统', algo.dasha_system || ''],
['映射系统', algo.pada_system || ''],
['分盘系统', algo.division_system || ''],
['障碍系统', algo.argala_system || ''],
];
for (const [label, value] of algoLines) {
lines.push(` ${(label+':').padEnd(10)} ${value}`);
}
lines.push('');
lines.push(` ★ 重要声明: ${algo.note || ''}`);
lines.push('');
// ═══════════ INPUT ═══════════
lines.push('二、【输入数据】');
lines.push(sub);
lines.push(` 出生日期 (UTC): ${inp.date || ''}`);
lines.push(` 出生时间 (UTC): ${inp.time || ''}`);
lines.push(` 本地时区: UTC${(inp.tz||0)>=0?'+':''}${inp.tz||0}`);
lines.push(` 纬度: ${(inp.lat||0).toFixed(4)}°`);
lines.push(` 经度: ${(inp.lon||0).toFixed(4)}°`);
lines.push(` 姓名/备注: ${inp.name || '(未命名)'}`);
lines.push(` 宫位制: ${algo.house_system || inp.house_system || 'Whole Sign'}`);
lines.push('');
// ═══════════ ASCENDANT ═══════════
if (asc.sign) {
lines.push(` 上升点 (Asc): ${asc.sign_full || asc.sign} ${(asc.sign_deg||0).toFixed(2)}° [${signCN[asc.sign]||asc.sign}]`);
lines.push('');
}
// ═══════════ PANCHANGA ═══════════
const p = d.panchanga || {};
if (p.tithi) {
lines.push('三、【Panchanga 五支历法】');
lines.push(sub);
const t = p.tithi;
lines.push(` Tithi (月日) ${t.name.padEnd(14)} ${t.paksha_chinese}·第${t.paksha_tithi}日 已过${(t.progress*100).toFixed(1)}%`);
lines.push(` 解释: 月日指示月亮相对于太阳的位置,管理每日吉凶`);
const n = p.nakshatra;
lines.push(` Nakshatra (星宿) ${n.name.padEnd(14)} 第${n.pada}/4足 Lord: ${n.lord} (${n.lord_full}) 已过${(n.progress*100).toFixed(1)}%`);
lines.push(` 解释: ${n.name_chinese}(${n.meaning}), 27星宿之一,是月亮当晚所驻之星座`);
const y = p.yoga;
lines.push(` Nitya Yoga (合朔) ${y.name.padEnd(14)} ${y.name_chinese} — ${y.meaning}`);
const k = p.karana;
lines.push(` Karana (半日) ${k.name.padEnd(14)} ${k.name_chinese} [${k.type}] 第${k.half}半`);
const v = p.vara;
lines.push(` Vara (曜日) ${v.name.padEnd(14)} ${v.name_chinese} ${v.planet_chinese}`);
lines.push('');
}
// ═══════════ PLANETS ═══════════
if (d.planets) {
lines.push('四、【行星位置 — 回归黄道】');
lines.push(sub);
const header = ` ${'行星'.padEnd(8)}${'符号'.padEnd(6)}${'黄经 °'.padEnd(13)}${'星座'.padEnd(8)}${'星宿(Nak)'.padEnd(14)}${'速度°/d'.padEnd(10)}${'方向'}`;
lines.push(header);
lines.push(` ${'-'.repeat(62)}`);
const order = ['Su','Mo','Ma','Me','Ju','Ve','Sa','Ra','Ke','Ur','Ne','Pl'];
for (const key of order) {
const pl = d.planets[key];
if (!pl) continue;
const cn = pcn[key] || '';
const retro = pl.retrograde ? 'R 逆行' : 'D 顺行';
const degDMS = toDMS(pl.lon);
lines.push(` ${cn.padEnd(8)}${key.padEnd(6)}${degDMS.padEnd(13)}${(pl.sign+'').padEnd(8)}${''.padEnd(14)}${(pl.speed||0).toFixed(4).padEnd(10)}${retro}`);
}
lines.push('');
lines.push(` 注: 黄经为回归黄道(Tropical/Sayana)经度,未做任何岁差修正`);
lines.push('');
}
// ═══════════ HOUSES ═══════════
if (d.houses) {
lines.push('五、【宫位 — 整宫制 Whole Sign Houses】');
lines.push(sub);
const bhavaCN = {1:'命宫',2:'财帛',3:'兄弟',4:'田宅',5:'子女',6:'疾病',7:'夫妻',8:'死亡',9:'福德',10:'事业',11:'朋友',12:'损失'};
for (const h of d.houses) {
const pls = (h.planets||[]).map(p => p + ' ' + (pcn[p]||'')).join(', ') || '(空)';
const bhava = bhavaCN[h.house] || '';
lines.push(` H${String(h.house).padStart(2)} ${bhava.padEnd(6)} | ${(h.sign+'').padEnd(5)} ${(signCN[h.sign]||'').padEnd(4)} | ${pls}`);
}
lines.push('');
}
// ═══════════ KARAKAS ═══════════
if (d.karakas_7) {
lines.push('六、【Chara Karaka — 七曜变格主星】');
lines.push(sub);
lines.push(' Jaimini 体系核心: 以行星在星座内的度数排序,度数最高者为灵魂主星(AK)。');
lines.push(' 此排序随出生时间而变,是 Jaimini 区别于 Parashara(固定主星)的标志。');
lines.push('');
for (const k of d.karakas_7) {
lines.push(` ${k.karaka.padEnd(4)} ${k.karaka_full.padEnd(20)} = ${k.planet} ${(pcn[k.planet]||'').padEnd(6)} | ${(k.sign+'').padEnd(5)} ${k.degree_in_sign.toFixed(4)}° °`);
}
lines.push('');
lines.push(' Sthira Karaka (固定主星, 仅参考):');
lines.push(' Su=父亲(PK) Mo=母亲(MK) Ma=兄弟(BK) Me=事业(AmK)');
lines.push(' Ju=财富(PK) Ve=配偶(DK) Sa=苦修(GK/DK)');
lines.push('');
lines.push(' ★ 解读提示: AK 代表灵魂方向, AmK 代表心智, BK 代表勇气, DK 代表婚姻。');
lines.push(' 每个 Karaka 的星座/宫位/分盘位置决定了该领域的吉凶。');
lines.push('');
}
// ═══════════ DASHA ═══════════
if (d.dasha_years) {
lines.push('七、【Chara Dasha — Jaimini 大运系统】');
lines.push(sub);
lines.push(' 计算方法: 以第9宫(Pitri Bhava)主星所在星座为起点,');
lines.push(' Prakriti Chakra 序列(奇宫顺数/偶宫逆数)。');
lines.push('');
lines.push(' ── 12 星座大运年数 ──');
for (const dy of d.dasha_years) {
const bar = '█'.repeat(Math.max(1, Math.round(dy.years)));
lines.push(` ${dy.sign_name.padEnd(6)} Lord:${dy.lord.padEnd(4)} ${dy.years.toString().padStart(2)}年 ${bar}`);
}
lines.push('');
}
// Chara Dasha timeline
if (d.chara_dasha && d.chara_dasha.length) {
lines.push(' ── 大运时间线 (Mahadasha) ──');
for (const md of d.chara_dasha) {
lines.push(` ${md.sign_name.padEnd(6)} ${md.lord} ${md.years.toFixed(2)}年 | Antar: ${(md.antar||[]).map(a => a.sign_name+'('+a.lord+'):'+a.years.toFixed(2)+'y').join(' ')}`);
}
lines.push('');
}
// ═══════════ PADAS ═══════════
if (d.padas && d.padas['1']) {
lines.push('八、【Arudha Padas — 映射点系统】');
lines.push(sub);
lines.push(' Arudha = 宫主星所在星座 + (宫主星所在星座 - 宫位星座)');
lines.push(' 若落在第1宫或第7宫,则移至第10宫(例外规则)。');
lines.push('');
lines.push(' 全部 12 宫的 Arudha:');
lines.push(` ${'宫位'.padEnd(6)}${'Pada名称'.padEnd(10)}${'Arudha星座'.padEnd(14)}${'Lord'.padEnd(8)}`);
lines.push(` ${'-'.repeat(40)}`);
const padaNames = {1:'AL (命)',2:'A2 (财)',3:'A3 (兄)',4:'A4 (田)',5:'A5 (子)',6:'A6 (病)',7:'A7 (妻)',8:'A8 (死)',9:'A9 (福)',10:'A10 (事)',11:'A11 (友)',12:'UL (姻)'};
for (let hnum = 1; hnum <= 12; hnum++) {
const pd = d.padas[String(hnum)];
if (!pd) continue;
lines.push(` H${hnum.toString().padStart(2)} ${(padaNames[hnum]||'').padEnd(10)} ${(pd.sign_full||pd.sign).padEnd(14)} ${(pd.lord||'').padEnd(8)}`);
}
lines.push('');
// Upapada detail
if (d.upapada) {
lines.push(` ★ Upapada (UL): ${d.upapada.sign_full} Lord: ${d.upapada.lord}`);
lines.push(` ${d.upapada.description || ''}`);
}
lines.push('');
}
// ═══════════ SPECIAL LAGNAS ═══════════
if (d.special_lagnas) {
const sl = d.special_lagnas;
const hasLagnas = (sl.HL && sl.HL.sign) || (sl.GL && sl.GL.sign) || (sl.VL && sl.VL.sign);
if (hasLagnas) {
lines.push('九、【特殊上升点 — Special Lagnas】');
lines.push(sub);
if (sl.HL && sl.HL.sign) {
lines.push(` Hora Lagna (HL): ${sl.HL.sign_full || sl.HL.sign} Lord: ${sl.HL.lord || ''}`);
lines.push(` 解释: 每约1小时变化一次,代表财富和繁荣。用于财运判断。`);
}
if (sl.GL && sl.GL.sign) {
lines.push(` Ghatika Lagna (GL): ${sl.GL.sign_full || sl.GL.sign} Lord: ${sl.GL.lord || ''}`);
lines.push(` 解释: 每约24分钟(Ghati)变化一次,代表权力和权威。用于政运判断。`);
}
if (sl.VL && sl.VL.sign) {
lines.push(` Varnada Lagna (VL): ${sl.VL.sign_full || sl.VL.sign} Lord: ${sl.VL.lord || ''}`);
lines.push(` 解释: 由上升星座派生(asc_sign × 3 mod 12),代表社会地位和阶层。`);
}
lines.push('');
}
}
// ═══════════ DIVISIONS ═══════════
if (d.divisions) {
lines.push('十、【Jaimini 分盘 — Varga Divisions】');
lines.push(sub);
lines.push(' Jaimini 分盘遵循 "阳顺阴逆" 原则: 阳星座顺排/阴星座逆排。');
lines.push('');
// D-9 Navamsa
if (d.divisions.D9) {
lines.push(' ── D-9 Navamsa (九分盘, 配偶/内在自我) ──');
lines.push(' 每星座 9 等分 (3°20\' 每份)');
for (const [name, dv] of Object.entries(d.divisions.D9)) {
const label = name === 'Asc' ? '上升' : (pcn[name]||name);
lines.push(` ${label.padEnd(8)} → ${(dv.sign+'').padEnd(5)} ${signCN[dv.sign]||''}`);
}
lines.push('');
}
// D-3 Drekkana
if (d.divisions.D3) {
lines.push(' ── D-3 Drekkana (三分盘, 兄弟姐妹) ──');
lines.push(' 每星座 3 等分 (10° 每份), Parivrittitraya 三重旋转法');
for (const [name, dv] of Object.entries(d.divisions.D3)) {
const label = name === 'Asc' ? '上升' : (pcn[name]||name);
lines.push(` ${label.padEnd(8)} → ${(dv.sign+'').padEnd(5)} ${signCN[dv.sign]||''}`);
}
lines.push('');
}
// D-12 Dwadashamsha
if (d.divisions.D12) {
lines.push(' ── D-12 Dwadashamsha (十二分盘, 父母) ──');
lines.push(' 每星座 12 等分 (2°30\' 每份)');
for (const [name, dv] of Object.entries(d.divisions.D12)) {
const label = name === 'Asc' ? '上升' : (pcn[name]||name);
lines.push(` ${label.padEnd(8)} → ${(dv.sign+'').padEnd(5)} ${signCN[dv.sign]||''}`);
}
lines.push('');
}
}
// ═══════════ ARGALA ═══════════
if (d.argalas) {
lines.push('十一、【Argala 障碍分析 — Jaimini 判断系统】');
lines.push(sub);
lines.push(' Primary Argala: 宫位 2、4、11 (从参考宫位起算)');
lines.push(' Virodhargala (反障碍): 宫位 12、10、3 (分别阻碍 2、4、11)');
lines.push(' Secondary Argala: 宫位 5、9');
lines.push('');
lines.push(` ${'宫位'.padEnd(6)}${'Primary'.padEnd(20)}${'Virodhargala'.padEnd(20)}${'Secondary'.padEnd(16)}${'净结果'}`);
lines.push(` ${'-'.repeat(65)}`);
const pLabels = {H2:'2', H4:'4', H11:'11'};
const vLabels = {H12:'12', H10:'10', H3:'3'};
const sLabels = {H5:'5', H9:'9'};
for (let hnum = 1; hnum <= 12; hnum++) {
const a = d.argalas[String(hnum)];
if (!a) continue;
const pri = Object.entries(a.primary||{}).map(([k,v]) => `${k}:${v.planet||'—'}`).join(' ') || '无';
const vir = Object.entries(a.virodhargala||{}).map(([k,v]) => `${k}:${v.planet||'—'}`).join(' ') || '无';
const sec = Object.entries(a.secondary||{}).map(([k,v]) => `${k}:${v.planet||'—'}`).join(' ') || '无';
const net = {supported:'★ 吉', obstructed:'▼ 凶', neutral:'— 平'}[a.net_result] || a.net_result;
lines.push(` H${hnum.toString().padStart(2)} ${pri.padEnd(20)}${vir.padEnd(20)}${sec.padEnd(16)}${net}`);
}
lines.push('');
}
// ═══════════ RAJAYOGA ═══════════
if (d.lagna_rajayoga || d.karakamsa_rajayoga) {
lines.push('十二、【Rajayoga — 王者瑜伽总结】');
lines.push(sub);
if (d.lagna_rajayoga) {
const lr = d.lagna_rajayoga;
const levels = {1:'Padargala (弱)', 2:'Ardhargala (中)', 3:'Tripadargala (强)', 4:'Poornargala (极强)'};
lines.push(` Lagna Argala Rajayoga: ${lr.type} — ${levels[lr.level] || 'Level '+lr.level}`);
lines.push(` 解读: H1的Argala意味着第2/4/11宫有行星占据,带来对命宫的支持。`);
lines.push(` Level ${lr.level}: ${lr.desc}`);
}
if (d.karakamsa_rajayoga) {
const kr = d.karakamsa_rajayoga;
lines.push(` Karakamsa Rajayoga: ${kr.is_rajayoga?'★ 存在王者瑜伽':'— 未形成'} (Level ${kr.yoga_level})`);
lines.push(` Atmakaraka: ${kr.ak_planet} | Karakamsa Sign: ${kr.karakamsa_sign}`);
lines.push(` 解读: ${kr.description}`);
}
lines.push('');
}
// ═══════════ KARAKAMSA ═══════════
lines.push('十三、【Jaimini 解读指引】');
lines.push(sub);
lines.push(' 本报告基于纯 Jaimini 体系(Iranganti Rangacharya 方法):');
lines.push(' 1. 以 Chara Karaka(变格主星)为核心,7 行星按度排序取 AK~DK');
lines.push(' 2. 大运使用 Chara Dasha(Prakriti Chakra),非 Vimshottari');
lines.push(' 3. 宫位使用整宫制(Whole Sign),Rasi = Bhava');
lines.push(' 4. 映射点 Arudha Pada 用于判断世俗层面的表现');
lines.push(' 5. Argala 系统用于判断宫位间的支持与阻碍');
lines.push(' 6. D-9 Navamsa 用于判断内在自我、婚姻质量');
lines.push(' 7. 所有星体位置为回归黄道(Tropical),非恒星黄道(Sidereal)');
lines.push('');
// ═══════════ FOOTER ═══════════
lines.push(sep);
lines.push(` 导出时间: ${new Date().toISOString()}`);
lines.push(` ★ 重要提醒: 若使用本报告数据与其他占星软件对比,请务必确认对方使用`);
lines.push(` 相同的算法体系。纯 Jaimini 回归黄道的结果与 Parashara/恒星黄道/`);
lines.push(` Vimshottari Dasha 体系的结果完全不具可比性。混用体系必导致判读错误。`);
lines.push(sep);
downloadBlob(lines.join('\n'), getExportFilename('txt'), 'text/plain;charset=utf-8');
}
// Helper: decimal degrees → DMS string
function toDMS(deg) {
const d = Math.floor(deg);
const mf = (deg - d) * 60;
const m = Math.floor(mf);
const s = ((mf - m) * 60).toFixed(1);
return `${d}°${String(m).padStart(2,'0')}'${String(s).padStart(4,'0')}"`;
}
// ===== Export Buttons =====
const exportJsonBtn = document.getElementById('export-json-btn');
const exportTxtBtn = document.getElementById('export-txt-btn');
if (exportJsonBtn) exportJsonBtn.addEventListener('click', exportJSON);
if (exportTxtBtn) exportTxtBtn.addEventListener('click', exportTXT);
// ===== Locate Button =====
if (locateBtn) {
locateBtn.addEventListener('click', locateUser);
}
// ===== Init =====
document.addEventListener('DOMContentLoaded', function() {
initDateTime();
initMap();
});