Files
Jyotisha/jyotish-app/jyotish-engine.js
T
732642856 6c20faffd5 fix: 前端 JS computeDasha 同步 balance/full_years/is_balance 字段
与后端 Python cmd_dasha 保持一致:
- 第一个 MD 的 years 字段显示 balance 值
- 新增 full_years / is_balance / balance_years / elapsed_at_birth 字段
- 日期计算保持使用完整年数(数学等价性不变)
2026-05-04 23:01:46 +08:00

691 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Jyotish Engine — JavaScript 版 v2.0
* 从 Python jyotish_engine.py v3.7.1 移植 + 扩展
* 底层天文计算使用 swisseph-wasm (Swiss Ephemeris WASM)
*
* v2.0: +Combustion, +MC, 支持高级计算模块导入
*/
// ============================================================================
// 常量(与Python引擎完全一致)
// ============================================================================
export const NAKSHATRA_LIST = [
{ name: "Ashwini", lord: "Ketu", years: 7 },
{ name: "Bharani", lord: "Venus", years: 20 },
{ name: "Krittika", lord: "Sun", years: 6 },
{ name: "Rohini", lord: "Moon", years: 10 },
{ name: "Mrigashira", lord: "Mars", years: 7 },
{ name: "Ardra", lord: "Rahu", years: 18 },
{ name: "Punarvasu", lord: "Jupiter", years: 16 },
{ name: "Pushya", lord: "Saturn", years: 19 },
{ name: "Ashlesha", lord: "Mercury", years: 17 },
{ name: "Magha", lord: "Ketu", years: 7 },
{ name: "Purva Phalguni", lord: "Venus", years: 20 },
{ name: "Uttara Phalguni", lord: "Sun", years: 6 },
{ name: "Hasta", lord: "Moon", years: 10 },
{ name: "Chitra", lord: "Mars", years: 7 },
{ name: "Swati", lord: "Rahu", years: 18 },
{ name: "Vishakha", lord: "Jupiter", years: 16 },
{ name: "Anuradha", lord: "Saturn", years: 19 },
{ name: "Jyeshtha", lord: "Mercury", years: 17 },
{ name: "Mula", lord: "Ketu", years: 7 },
{ name: "Purva Ashadha", lord: "Venus", years: 20 },
{ name: "Uttara Ashadha", lord: "Sun", years: 6 },
{ name: "Shravana", lord: "Moon", years: 10 },
{ name: "Dhanishta", lord: "Mars", years: 7 },
{ name: "Shatabhisha", lord: "Rahu", years: 18 },
{ name: "Purva Bhadrapada", lord: "Jupiter", years: 16 },
{ name: "Uttara Bhadrapada", lord: "Saturn", years: 19 },
{ name: "Revati", lord: "Mercury", years: 17 },
];
export const DASHA_ORDER = ["Ketu", "Venus", "Sun", "Moon", "Mars", "Rahu", "Jupiter", "Saturn", "Mercury"];
export const DASHA_YEARS = { Ketu: 7, Venus: 20, Sun: 6, Moon: 10, Mars: 7, Rahu: 18, Jupiter: 16, Saturn: 19, Mercury: 17 };
export const SIGNS = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo', 'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces'];
export const SIGNS_CN = {
Aries: '白羊座', Taurus: '金牛座', Gemini: '双子座', Cancer: '巨蟹座',
Leo: '狮子座', Virgo: '处女座', Libra: '天秤座', Scorpio: '天蝎座',
Sagittarius: '射手座', Capricorn: '摩羯座', Aquarius: '水瓶座', Pisces: '双鱼座'
};
export const SIGNS_EN_SHORT = {
Aries: 'Ari', Taurus: 'Tau', Gemini: 'Gem', Cancer: 'Can',
Leo: 'Leo', Virgo: 'Vir', Libra: 'Lib', Scorpio: 'Sco',
Sagittarius: 'Sag', Capricorn: 'Cap', Aquarius: 'Aqu', Pisces: 'Pis'
};
export const SIGN_LORDS = {
Aries: 'Mars', Taurus: 'Venus', Gemini: 'Mercury', Cancer: 'Moon',
Leo: 'Sun', Virgo: 'Mercury', Libra: 'Venus', Scorpio: 'Mars',
Sagittarius: 'Jupiter', Capricorn: 'Saturn', Aquarius: 'Saturn', Pisces: 'Jupiter'
};
export const EXALTATION = { Sun: 'Aries', Moon: 'Taurus', Mars: 'Capricorn', Mercury: 'Virgo', Jupiter: 'Cancer', Venus: 'Pisces', Saturn: 'Libra' };
export const DEBILITATION = { Sun: 'Libra', Moon: 'Scorpio', Mars: 'Cancer', Mercury: 'Pisces', Jupiter: 'Capricorn', Venus: 'Virgo', Saturn: 'Aries' };
export const PLANET_CN = {
Sun: "太阳", Moon: "月亮", Mars: "火星", Mercury: "水星",
Jupiter: "木星", Venus: "金星", Saturn: "土星", Rahu: "北交点", Ketu: "南交点"
};
export const PLANET_SYMBOLS = {
Sun: '☉', Moon: '☽', Mars: '♂', Mercury: '☿',
Jupiter: '♃', Venus: '♀', Saturn: '♄', Rahu: '☊', Ketu: '☋'
};
// 行星特殊相位规则(Graha Drishti
export const PLANET_ASPECTS = {
Sun: [7], Moon: [7], Mars: [4, 7, 8], Mercury: [7],
Jupiter: [5, 7, 9], Venus: [7], Saturn: [3, 7, 10],
Rahu: [5, 7, 9], Ketu: [5, 7, 9],
};
// 行星关系
export const PLANET_RELATIONS = {
Sun: { friends: ['Moon','Mars','Jupiter'], enemies: ['Venus','Saturn'] },
Moon: { friends: ['Sun','Mercury'], enemies: [] },
Mars: { friends: ['Sun','Moon','Jupiter'], enemies: ['Mercury'] },
Mercury: { friends: ['Sun','Venus'], enemies: ['Moon'] },
Jupiter: { friends: ['Sun','Moon','Mars'], enemies: ['Mercury','Venus'] },
Venus: { friends: ['Mercury','Saturn'], enemies: ['Sun','Moon'] },
Saturn: { friends: ['Mercury','Venus'], enemies: ['Sun','Moon','Mars'] },
};
// Swiss Ephemeris constants
const SE_SUN = 0, SE_MOON = 1, SE_MARS = 4, SE_MERCURY = 2, SE_JUPITER = 5, SE_VENUS = 3, SE_SATURN = 6, SE_MEAN_NODE = 10;
const SE_SIDM_LAHIRI = 1;
const SEFLG_SPEED = 256;
const PLANETS_SWE = {
Sun: SE_SUN, Moon: SE_MOON, Mars: SE_MARS, Mercury: SE_MERCURY,
Jupiter: SE_JUPITER, Venus: SE_VENUS, Saturn: SE_SATURN, Rahu: SE_MEAN_NODE
};
// 城市数据库
export const CITIES = [
// ═══ 中国(含港澳台)═══
{name:"北京",en:"Beijing",lat:39.9,lon:116.4,tz:8},
{name:"上海",en:"Shanghai",lat:31.2,lon:121.5,tz:8},
{name:"广州",en:"Guangzhou",lat:23.1,lon:113.3,tz:8},
{name:"深圳",en:"Shenzhen",lat:22.5,lon:114.1,tz:8},
{name:"REDACTED_PLACE",en:"REDACTED_PLACE",lat:36.6,lon:114.5,tz:8},
{name:"成都",en:"Chengdu",lat:30.6,lon:104.1,tz:8},
{name:"杭州",en:"Hangzhou",lat:30.3,lon:120.2,tz:8},
{name:"南京",en:"Nanjing",lat:32.1,lon:118.8,tz:8},
{name:"武汉",en:"Wuhan",lat:30.6,lon:114.3,tz:8},
{name:"西安",en:"Xian",lat:34.3,lon:108.9,tz:8},
{name:"重庆",en:"Chongqing",lat:29.6,lon:106.5,tz:8},
{name:"天津",en:"Tianjin",lat:39.1,lon:117.2,tz:8},
{name:"苏州",en:"Suzhou",lat:31.3,lon:120.6,tz:8},
{name:"长沙",en:"Changsha",lat:28.2,lon:113.0,tz:8},
{name:"郑州",en:"Zhengzhou",lat:34.7,lon:113.7,tz:8},
{name:"沈阳",en:"Shenyang",lat:41.8,lon:123.4,tz:8},
{name:"大连",en:"Dalian",lat:38.9,lon:121.6,tz:8},
{name:"哈尔滨",en:"Harbin",lat:45.8,lon:126.5,tz:8},
{name:"长春",en:"Changchun",lat:43.9,lon:125.3,tz:8},
{name:"济南",en:"Jinan",lat:36.7,lon:117.0,tz:8},
{name:"青岛",en:"Qingdao",lat:36.1,lon:120.4,tz:8},
{name:"福州",en:"Fuzhou",lat:26.1,lon:119.3,tz:8},
{name:"厦门",en:"Xiamen",lat:24.5,lon:118.1,tz:8},
{name:"昆明",en:"Kunming",lat:25.0,lon:102.7,tz:8},
{name:"贵阳",en:"Guiyang",lat:26.6,lon:106.7,tz:8},
{name:"南宁",en:"Nanning",lat:22.8,lon:108.3,tz:8},
{name:"海口",en:"Haikou",lat:20.0,lon:110.3,tz:8},
{name:"三亚",en:"Sanya",lat:18.3,lon:109.5,tz:8},
{name:"兰州",en:"Lanzhou",lat:36.1,lon:103.8,tz:8},
{name:"乌鲁木齐",en:"Urumqi",lat:43.8,lon:87.6,tz:8},
{name:"拉萨",en:"Lhasa",lat:29.7,lon:91.1,tz:8},
{name:"呼和浩特",en:"Hohhot",lat:40.8,lon:111.7,tz:8},
{name:"石家庄",en:"Shijiazhuang",lat:38.0,lon:114.5,tz:8},
{name:"太原",en:"Taiyuan",lat:37.9,lon:112.6,tz:8},
{name:"合肥",en:"Hefei",lat:31.8,lon:117.3,tz:8},
{name:"南昌",en:"Nanchang",lat:28.7,lon:115.9,tz:8},
{name:"无锡",en:"Wuxi",lat:31.6,lon:120.3,tz:8},
{name:"宁波",en:"Ningbo",lat:29.9,lon:121.6,tz:8},
{name:"佛山",en:"Foshan",lat:23.0,lon:113.1,tz:8},
{name:"东莞",en:"Dongguan",lat:23.0,lon:113.7,tz:8},
{name:"珠海",en:"Zhuhai",lat:22.3,lon:113.6,tz:8},
{name:"温州",en:"Wenzhou",lat:28.0,lon:120.7,tz:8},
{name:"保定",en:"Baoding",lat:38.9,lon:115.5,tz:8},
{name:"唐山",en:"Tangshan",lat:39.6,lon:118.2,tz:8},
{name:"烟台",en:"Yantai",lat:37.5,lon:121.4,tz:8},
{name:"洛阳",en:"Luoyang",lat:34.6,lon:112.5,tz:8},
{name:"常州",en:"Changzhou",lat:31.8,lon:119.9,tz:8},
{name:"徐州",en:"Xuzhou",lat:34.3,lon:117.2,tz:8},
{name:"盐城",en:"Yancheng",lat:33.4,lon:120.1,tz:8},
{name:"秦皇岛",en:"Qinhuangdao",lat:39.9,lon:119.6,tz:8},
{name:"银川",en:"Yinchuan",lat:38.5,lon:106.3,tz:8},
{name:"西宁",en:"Xining",lat:36.6,lon:101.8,tz:8},
// 港澳台
{name:"香港",en:"Hong Kong",lat:22.3,lon:114.2,tz:8},
{name:"澳门",en:"Macau",lat:22.2,lon:113.5,tz:8},
{name:"台北",en:"Taipei",lat:25.0,lon:121.5,tz:8},
{name:"高雄",en:"Kaohsiung",lat:22.6,lon:120.3,tz:8},
{name:"台中",en:"Taichung",lat:24.1,lon:120.7,tz:8},
// ═══ 东亚 ═══
{name:"东京",en:"Tokyo",lat:35.7,lon:139.7,tz:9},
{name:"大阪",en:"Osaka",lat:34.7,lon:135.5,tz:9},
{name:"京都",en:"Kyoto",lat:35.0,lon:135.8,tz:9},
{name:"横滨",en:"Yokohama",lat:35.4,lon:139.6,tz:9},
{name:"名古屋",en:"Nagoya",lat:35.2,lon:136.9,tz:9},
{name:"札幌",en:"Sapporo",lat:43.1,lon:141.3,tz:9},
{name:"福冈",en:"Fukuoka",lat:33.6,lon:130.4,tz:9},
{name:"首尔",en:"Seoul",lat:37.6,lon:127.0,tz:9},
{name:"釜山",en:"Busan",lat:35.2,lon:129.0,tz:9},
{name:"仁川",en:"Incheon",lat:37.4,lon:126.7,tz:9},
// ═══ 东南亚 ═══
{name:"新加坡",en:"Singapore",lat:1.4,lon:103.8,tz:8},
{name:"曼谷",en:"Bangkok",lat:13.8,lon:100.5,tz:7},
{name:"吉隆坡",en:"Kuala Lumpur",lat:3.1,lon:101.7,tz:8},
{name:"雅加达",en:"Jakarta",lat:-6.2,lon:106.8,tz:7},
{name:"马尼拉",en:"Manila",lat:14.6,lon:121.0,tz:8},
{name:"胡志明市",en:"Ho Chi Minh City",lat:10.8,lon:106.7,tz:7},
{name:"河内",en:"Hanoi",lat:21.0,lon:105.9,tz:7},
{name:"金边",en:"Phnom Penh",lat:11.6,lon:104.9,tz:7},
// ═══ 南亚 ═══
{name:"新德里",en:"New Delhi",lat:28.6,lon:77.2,tz:5.5},
{name:"孟买",en:"Mumbai",lat:19.1,lon:72.9,tz:5.5},
{name:"加尔各答",en:"Kolkata",lat:22.6,lon:88.4,tz:5.5},
{name:"金奈",en:"Chennai",lat:13.1,lon:80.3,tz:5.5},
{name:"班加罗尔",en:"Bangalore",lat:13.0,lon:77.6,tz:5.5},
{name:"瓦拉纳西",en:"Varanasi",lat:25.3,lon:83.0,tz:5.5},
// ═══ 中东 ═══
{name:"迪拜",en:"Dubai",lat:25.2,lon:55.3,tz:4},
{name:"阿布扎比",en:"Abu Dhabi",lat:24.5,lon:54.7,tz:4},
{name:"多哈",en:"Doha",lat:25.3,lon:51.5,tz:3},
{name:"利雅得",en:"Riyadh",lat:24.7,lon:46.7,tz:3},
{name:"伊斯坦布尔",en:"Istanbul",lat:41.0,lon:29.0,tz:3},
{name:"特拉维夫",en:"Tel Aviv",lat:32.1,lon:34.8,tz:2},
{name:"德黑兰",en:"Tehran",lat:35.7,lon:51.4,tz:3.5},
// ═══ 欧洲 ═══
{name:"伦敦",en:"London",lat:51.5,lon:-0.1,tz:0},
{name:"巴黎",en:"Paris",lat:48.9,lon:2.3,tz:1},
{name:"柏林",en:"Berlin",lat:52.5,lon:13.4,tz:1},
{name:"马德里",en:"Madrid",lat:40.4,lon:-3.7,tz:1},
{name:"罗马",en:"Rome",lat:41.9,lon:12.5,tz:1},
{name:"阿姆斯特丹",en:"Amsterdam",lat:52.4,lon:4.9,tz:1},
{name:"布鲁塞尔",en:"Brussels",lat:50.9,lon:4.4,tz:1},
{name:"维也纳",en:"Vienna",lat:48.2,lon:16.4,tz:1},
{name:"苏黎世",en:"Zurich",lat:47.4,lon:8.5,tz:1},
{name:"日内瓦",en:"Geneva",lat:46.2,lon:6.1,tz:1},
{name:"慕尼黑",en:"Munich",lat:48.1,lon:11.6,tz:1},
{name:"法兰克福",en:"Frankfurt",lat:50.1,lon:8.7,tz:1},
{name:"巴塞罗那",en:"Barcelona",lat:41.4,lon:2.2,tz:1},
{name:"里斯本",en:"Lisbon",lat:38.7,lon:-9.1,tz:0},
{name:"雅典",en:"Athens",lat:37.98,lon:23.7,tz:2},
{name:"莫斯科",en:"Moscow",lat:55.8,lon:37.6,tz:3},
{name:"圣彼得堡",en:"Saint Petersburg",lat:59.9,lon:30.3,tz:3},
{name:"华沙",en:"Warsaw",lat:52.2,lon:21.0,tz:1},
{name:"布拉格",en:"Prague",lat:50.1,lon:14.4,tz:1},
{name:"布达佩斯",en:"Budapest",lat:47.5,lon:19.0,tz:1},
{name:"哥本哈根",en:"Copenhagen",lat:55.7,lon:12.6,tz:1},
{name:"斯德哥尔摩",en:"Stockholm",lat:59.3,lon:18.1,tz:1},
{name:"赫尔辛基",en:"Helsinki",lat:60.2,lon:24.9,tz:2},
{name:"都柏林",en:"Dublin",lat:53.3,lon:-6.3,tz:0},
{name:"爱丁堡",en:"Edinburgh",lat:55.9,lon:-3.2,tz:0},
// ═══ 北美 ═══
{name:"纽约",en:"New York",lat:40.7,lon:-74.0,tz:-5},
{name:"洛杉矶",en:"Los Angeles",lat:34.1,lon:-118.2,tz:-8},
{name:"旧金山",en:"San Francisco",lat:37.8,lon:-122.4,tz:-8},
{name:"芝加哥",en:"Chicago",lat:41.9,lon:-87.6,tz:-6},
{name:"华盛顿",en:"Washington DC",lat:38.9,lon:-77.0,tz:-5},
{name:"西雅图",en:"Seattle",lat:47.6,lon:-122.3,tz:-8},
{name:"波士顿",en:"Boston",lat:42.4,lon:-71.1,tz:-5},
{name:"迈阿密",en:"Miami",lat:25.8,lon:-80.2,tz:-5},
{name:"拉斯维加斯",en:"Las Vegas",lat:36.2,lon:-115.1,tz:-8},
{name:"圣地亚哥",en:"San Diego",lat:32.7,lon:-117.2,tz:-8},
{name:"休斯顿",en:"Houston",lat:29.8,lon:-95.4,tz:-6},
{name:"达拉斯",en:"Dallas",lat:32.8,lon:-96.8,tz:-6},
{name:"丹佛",en:"Denver",lat:39.7,lon:-105.0,tz:-7},
{name:"亚特兰大",en:"Atlanta",lat:33.7,lon:-84.4,tz:-5},
{name:"菲尼克斯",en:"Phoenix",lat:33.4,lon:-112.1,tz:-7},
{name:"波特兰",en:"Portland",lat:45.5,lon:-122.7,tz:-8},
{name:"多伦多",en:"Toronto",lat:43.7,lon:-79.4,tz:-5},
{name:"温哥华",en:"Vancouver",lat:49.3,lon:-123.1,tz:-8},
{name:"蒙特利尔",en:"Montreal",lat:45.5,lon:-73.6,tz:-5},
{name:"渥太华",en:"Ottawa",lat:45.4,lon:-75.7,tz:-5},
{name:"卡尔加里",en:"Calgary",lat:51.0,lon:-114.1,tz:-7},
{name:"墨西哥城",en:"Mexico City",lat:19.4,lon:-99.1,tz:-6},
// ═══ 南美 ═══
{name:"圣保罗",en:"Sao Paulo",lat:-23.6,lon:-46.6,tz:-3},
{name:"里约热内卢",en:"Rio de Janeiro",lat:-22.9,lon:-43.2,tz:-3},
{name:"布宜诺斯艾利斯",en:"Buenos Aires",lat:-34.6,lon:-58.4,tz:-3},
{name:"利马",en:"Lima",lat:-12.0,lon:-77.0,tz:-5},
{name:"波哥大",en:"Bogota",lat:4.7,lon:-74.1,tz:-5},
{name:"圣地亚哥",en:"Santiago",lat:-33.4,lon:-70.7,tz:-4},
// ═══ 大洋洲 ═══
{name:"悉尼",en:"Sydney",lat:-33.9,lon:151.2,tz:10},
{name:"墨尔本",en:"Melbourne",lat:-37.8,lon:145.0,tz:10},
{name:"布里斯班",en:"Brisbane",lat:-27.5,lon:153.0,tz:10},
{name:"珀斯",en:"Perth",lat:-31.9,lon:115.9,tz:8},
{name:"奥克兰",en:"Auckland",lat:-36.9,lon:174.8,tz:12},
{name:"惠灵顿",en:"Wellington",lat:-41.3,lon:174.8,tz:12},
// ═══ 非洲 ═══
{name:"开罗",en:"Cairo",lat:30.0,lon:31.2,tz:2},
{name:"约翰内斯堡",en:"Johannesburg",lat:-26.2,lon:28.0,tz:2},
{name:"开普敦",en:"Cape Town",lat:-33.9,lon:18.4,tz:2},
{name:"拉各斯",en:"Lagos",lat:6.5,lon:3.4,tz:1},
{name:"内罗毕",en:"Nairobi",lat:-1.3,lon:36.8,tz:3},
{name:"卡萨布兰卡",en:"Casablanca",lat:33.6,lon:-7.6,tz:1},
];
// ============================================================================
// 核心引擎
// ============================================================================
let sweInstance = null;
/**
* 等待 SwissEph 全局变量可用
*/
function waitForSwissEph(maxWait = 10000) {
return new Promise((resolve, reject) => {
if (window.SwissEph) { resolve(window.SwissEph); return; }
const start = Date.now();
const check = setInterval(() => {
if (window.SwissEph) { clearInterval(check); resolve(window.SwissEph); }
else if (Date.now() - start > maxWait) { clearInterval(check); reject(new Error('SwissEph not loaded after ' + maxWait + 'ms')); }
}, 100);
});
}
/**
* 初始化 Swiss Ephemeris WASM (swisseph-wasm 版本)
*/
export async function initEngine() {
if (sweInstance) return sweInstance;
const SwissEph = await waitForSwissEph();
sweInstance = new SwissEph();
await sweInstance.initSwissEph();
console.log('[Jyotish] Swiss Ephemeris initialized, version:', sweInstance.version());
return sweInstance;
}
/**
* 计算完整星盘 — 对应 Python compute_chart_data()
*/
export async function computeChart(birth) {
const swe = await initEngine();
const { year, month, day, hour, minute, lat, lon, tz } = birth;
const hourDecimal = hour + minute / 60.0 - tz;
// Julian Day (swe.julday 第5参数默认1=Gregorian)
const jd = swe.julday(year, month, day, hourDecimal);
console.log('[Jyotish] JD =', jd, 'hourUT =', hourDecimal);
// Ayanamsa (Lahiri)
swe.set_sid_mode(SE_SIDM_LAHIRI);
const ayanamsa = swe.get_ayanamsa(jd);
console.log('[Jyotish] Ayanamsa =', ayanamsa);
// Houses — 'W' = Whole Sign houses
const houseResult = swe.houses(jd, lat, lon, 'W');
const cusps = houseResult.cusps; // Float64Array[13], index 0 unused, 1-12 are cusps
const ascmc = houseResult.ascmc; // Float64Array[10], [0]=ASC, [1]=MC
// Ascendant (tropical → sidereal)
const ascTropical = ascmc[0];
const ascSidereal = ((ascTropical - ayanamsa) % 360 + 360) % 360;
const ascIdx = Math.floor(ascSidereal / 30);
const ascSign = SIGNS[ascIdx];
console.log('[Jyotish] ASC tropical =', ascTropical, 'sidereal =', ascSidereal, 'sign =', ascSign);
// MC (Midheaven)
const mcTropical = ascmc[1];
const mcSidereal = ((mcTropical - ayanamsa) % 360 + 360) % 360;
const mcIdx = Math.floor(mcSidereal / 30);
const result = {
birth_info: {
date: `${year}-${String(month).padStart(2,'0')}-${String(day).padStart(2,'0')}`,
time: `${String(hour).padStart(2,'0')}:${String(minute).padStart(2,'0')}`,
tz: `UTC${tz >= 0 ? '+' : ''}${tz}`,
lat, lon,
julian_day: Math.round(jd * 1e6) / 1e6,
ayanamsa: Math.round(ayanamsa * 1e4) / 1e4,
},
ascendant: {
sign: ascSign,
sign_cn: SIGNS_CN[ascSign],
degree: Math.round(ascSidereal * 1e4) / 1e4,
degree_in_sign: Math.round((ascSidereal - ascIdx * 30) * 1e4) / 1e4,
lord: SIGN_LORDS[ascSign],
},
mc: {
sign: SIGNS[mcIdx],
sign_cn: SIGNS_CN[SIGNS[mcIdx]],
degree: Math.round(mcSidereal * 1e4) / 1e4,
degree_in_sign: Math.round((mcSidereal - mcIdx * 30) * 1e4) / 1e4,
},
planets: {},
houses: {},
};
// House cusps (sidereal)
for (let i = 0; i < 12; i++) {
const cuspTropical = cusps[i + 1]; // cusps[1]..cusps[12]
if (cuspTropical == null) continue;
const cuspSidereal = ((cuspTropical - ayanamsa) % 360 + 360) % 360;
const si = Math.floor(cuspSidereal / 30);
result.houses[`house_${i + 1}`] = {
cusp_sign: SIGNS[si],
cusp_sign_cn: SIGNS_CN[SIGNS[si]],
cusp_degree: Math.round(cuspSidereal * 1e4) / 1e4,
lord: SIGN_LORDS[SIGNS[si]],
};
}
// Planet positions
// calc_ut 返回 Float64Array[6]: [longitude, latitude, distance, lonSpeed, latSpeed, distSpeed]
const nakSpan = 360.0 / 27;
const flags = SEFLG_SPEED;
for (const [pname, pid] of Object.entries(PLANETS_SWE)) {
try {
const pos = swe.calc_ut(jd, pid, flags);
// pos 是 Float64Array: [0]=lon, [1]=lat, [2]=dist, [3]=lonSpeed, [4]=latSpeed, [5]=distSpeed
const lonTropical = pos[0];
const lonSidereal = ((lonTropical - ayanamsa) % 360 + 360) % 360;
const spd = pos[3];
const si = Math.floor(lonSidereal / 30);
const dInS = lonSidereal - si * 30;
const sign = SIGNS[si];
const retro = spd < 0;
const house = ((si - ascIdx + 12) % 12) + 1;
let status = "中性";
if (EXALTATION[pname] === sign) status = "入旺";
else if (DEBILITATION[pname] === sign) status = "落陷";
else if (SIGN_LORDS[sign] === pname) status = "入庙";
const ni = Math.floor(lonSidereal / nakSpan);
const pada = Math.floor((lonSidereal % nakSpan) / (nakSpan / 4)) + 1;
const nak = NAKSHATRA_LIST[ni % 27];
result.planets[pname] = {
sign, sign_cn: SIGNS_CN[sign],
degree: Math.round(lonSidereal * 1e4) / 1e4,
degree_in_sign: Math.round(dInS * 1e4) / 1e4,
house, status, retrograde: retro,
speed: Math.round(spd * 1e6) / 1e6,
nakshatra: nak.name,
nakshatra_pada: pada,
nakshatra_lord: nak.lord,
combust: false,
};
// Ketu = opposite Rahu
if (pname === 'Rahu') {
const klon = (lonSidereal + 180) % 360;
const ksi = Math.floor(klon / 30);
const kd = klon - ksi * 30;
const kni = Math.floor(klon / nakSpan);
const kp = Math.floor((klon % nakSpan) / (nakSpan / 4)) + 1;
const kn = NAKSHATRA_LIST[kni % 27];
result.planets['Ketu'] = {
sign: SIGNS[ksi], sign_cn: SIGNS_CN[SIGNS[ksi]],
degree: Math.round(klon * 1e4) / 1e4,
degree_in_sign: Math.round(kd * 1e4) / 1e4,
house: ((ksi - ascIdx + 12) % 12) + 1,
status: "中性", retrograde: true,
speed: Math.round(spd * 1e6) / 1e6,
nakshatra: kn.name, nakshatra_pada: kp, nakshatra_lord: kn.lord,
combust: false,
};
}
console.log(`[Jyotish] ${pname}: ${sign} ${dInS.toFixed(2)}° (house ${house}, ${status})`);
} catch (e) {
console.error(`[Jyotish] Error computing ${pname}:`, e);
result.planets[pname] = { error: e.message };
}
}
return result;
}
// ============================================================================
// Dasha 计算
// ============================================================================
export function computeDasha(moonLon, birthDate, referenceDate) {
const nakSpan = 360.0 / 27;
const nakIdx = Math.floor(moonLon / nakSpan);
const progress = (moonLon % nakSpan) / nakSpan;
const nak = NAKSHATRA_LIST[nakIdx % 27];
const birthDt = new Date(birthDate);
const elapsed = progress * nak.years;
const startDt = new Date(birthDt.getTime() - elapsed * 365.25 * 24 * 3600000);
const startIdx = DASHA_ORDER.indexOf(nak.lord);
const remaining = nak.years - elapsed;
const timeline = [];
let dt = new Date(startDt.getTime());
for (let i = 0; i < 9; i++) {
const lord = DASHA_ORDER[(startIdx + i) % 9];
const years = DASHA_YEARS[lord];
const endDt = new Date(dt.getTime() + years * 365.25 * 24 * 3600000);
const isBalance = i === 0;
timeline.push({
lord, lord_cn: PLANET_CN[lord],
start: fmtDate(dt), end: fmtDate(endDt),
years: isBalance ? Math.round(remaining * 100) / 100 : years,
full_years: years,
is_balance: isBalance,
balance_years: isBalance ? Math.round(remaining * 100) / 100 : null,
elapsed_at_birth: isBalance ? Math.round(elapsed * 100) / 100 : null,
antardasha: null
});
dt = endDt;
}
const today = referenceDate ? new Date(referenceDate) : new Date();
let current = null;
for (const d of timeline) {
const ds = new Date(d.start), de = new Date(d.end);
if (ds <= today && today < de) {
const totalDays = (de - ds) / (24 * 3600000);
const li = DASHA_ORDER.indexOf(d.lord);
const sub = [];
let sdt = new Date(ds.getTime());
for (let j = 0; j < 9; j++) {
const sl = DASHA_ORDER[(li + j) % 9];
const sd = totalDays * DASHA_YEARS[sl] / 120;
const se = new Date(sdt.getTime() + sd * 24 * 3600000);
sub.push({ lord: sl, lord_cn: PLANET_CN[sl], start: fmtDate(sdt), end: fmtDate(se), is_current: sdt <= today && today < se });
sdt = se;
}
d.antardasha = sub;
current = d;
break;
}
}
return { moon_nakshatra: nak.name, birth_date: birthDate, reference_date: fmtDate(today), timeline, current_dasha: current };
}
// ============================================================================
// Yoga 识别
// ============================================================================
export function detectYogas(planets, ascSign) {
const ai = SIGNS.indexOf(ascSign);
const kl = [...new Set([1, 4, 7, 10].map(h => SIGN_LORDS[SIGNS[(ai + h - 1) % 12]]))];
const tl = [...new Set([1, 5, 9].map(h => SIGN_LORDS[SIGNS[(ai + h - 1) % 12]]))];
const yogas = [];
for (const k of kl) {
for (const t of tl) {
if (k !== t && planets[k] && planets[t] && planets[k].house === planets[t].house) {
yogas.push({ name: "Raja Yoga", name_cn: "王者格局", combination: `${PLANET_CN[k]}+${PLANET_CN[t]}同在第${planets[k].house}`, effects: ["权力地位", "事业成功", "社会影响力"], strength: "强" });
}
}
}
const yogaNames = { Mars: 'Ruchaka', Mercury: 'Bhadra', Jupiter: 'Hamsa', Venus: 'Malavya', Saturn: 'Sasa' };
for (const [p, info] of Object.entries(planets)) {
if ([1, 4, 7, 10].includes(info.house) && yogaNames[p]) {
if (EXALTATION[p] === info.sign || SIGN_LORDS[info.sign] === p) {
const st = EXALTATION[p] === info.sign ? "入旺" : "入庙";
yogas.push({ name: `${yogaNames[p]} Yoga`, name_cn: `${PLANET_CN[p]}${st}格局`, combination: `${PLANET_CN[p]}${st}${SIGNS_CN[info.sign]}(第${info.house}宫)`, effects: ["卓越才能", "领域领军", "人格魅力"], strength: st === "入旺" ? "极强" : "强" });
}
}
}
if (planets.Jupiter && planets.Moon) {
if ([1, 4, 7, 10].includes(planets.Jupiter.house) && [1, 4, 7, 10].includes(planets.Moon.house)) {
yogas.push({ name: "Gajakesari Yoga", name_cn: "象狮格局", combination: `木星第${planets.Jupiter.house}宫+月亮第${planets.Moon.house}`, effects: ["智慧学识", "财富名声", "道德品质"], strength: "中" });
}
}
for (const [p, info] of Object.entries(planets)) {
if (DEBILITATION[p] === info.sign) {
const dl = SIGN_LORDS[info.sign];
if (planets[dl] && [1, 4, 7, 10].includes(planets[dl].house)) {
yogas.push({ name: "Neechabhanga Raja Yoga", name_cn: "落陷取消格局", combination: `${PLANET_CN[p]}落陷在${SIGNS_CN[info.sign]}${PLANET_CN[dl]}在角宫化解`, effects: ["克服困难", "逆境崛起", "转化能力"], strength: "中强" });
}
}
}
const wh = [2, 5, 9, 11];
const wl = new Set(wh.map(h => SIGN_LORDS[SIGNS[(ai + h - 1) % 12]]));
let wc = 0;
for (const w of wl) { if (planets[w] && wh.includes(planets[w].house)) wc++; }
if (wc >= 2) {
yogas.push({ name: "Dhana Yoga", name_cn: "财富格局", combination: `${wc}个财富宫主星落入财富宫`, effects: ["财富积累", "物质成功", "投资收益"], strength: wc >= 3 ? "中强" : "中" });
}
return { ascendant: ascSign, yogas_detected: yogas.length, yogas };
}
/**
* 扩展 Yoga 检测 — 使用 interpretation.js 的 YOGA_DEFINITIONS
*/
export function detectExtendedYogas(planets, ascSign) {
const base = detectYogas(planets, ascSign);
// YOGA_DEFINITIONS 由 interpretation.js 动态导入
return base;
}
// ============================================================================
// Graha Drishti 行星相位
// ============================================================================
export function computeAspects(planets) {
const aspects = [];
const names = ['Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn','Rahu','Ketu'];
for (const from of names) {
const fp = planets[from]; if (!fp || fp.error) continue;
const asp = PLANET_ASPECTS[from] || [7];
for (const offset of asp) {
const toHouse = ((fp.house - 1 + offset) % 12) + 1;
const targets = names.filter(t => t !== from && planets[t] && !planets[t].error && planets[t].house === toHouse);
for (const to of targets) {
let type = offset === 7 ? 'opposition' : (offset === 5 || offset === 9) ? 'trine' : 'special';
let friendly = 'neutral';
const rel = PLANET_RELATIONS[from];
if (rel) {
if (rel.friends.includes(to)) friendly = 'friendly';
else if (rel.enemies.includes(to)) friendly = 'hostile';
}
aspects.push({ from, to, from_cn: PLANET_CN[from], to_cn: PLANET_CN[to], type, offset, toHouse, friendly });
}
}
}
return aspects;
}
// ============================================================================
// Navamsa (D9) 分盘
// ============================================================================
export function computeNavamsa(planets) {
const d9 = {};
for (const [pn, pi] of Object.entries(planets)) {
if (pi.error) continue;
const degInSign = pi.degree_in_sign;
const nw = 30 / 9;
const ni = Math.floor(degInSign / nw);
const si = SIGNS.indexOf(pi.sign);
let start;
if ([0,3,6,9].includes(si)) start = si; // Movable
else if ([1,4,7,10].includes(si)) start = (si+8)%12; // Fixed
else start = (si+4)%12; // Dual
const d9si = (start + ni) % 12;
d9[pn] = { sign: SIGNS[d9si], sign_cn: SIGNS_CN[SIGNS[d9si]], house: pi.house };
}
return { type: 'D9', name: 'Navamsa', name_cn: '九分盘', planets: d9 };
}
// ============================================================================
// 宫位综合分析
// ============================================================================
export function computeHouseAnalysis(planets, ascSign) {
const ai = SIGNS.indexOf(ascSign);
const houses = [];
for (let h = 1; h <= 12; h++) {
const si = (ai + h - 1) % 12;
const sign = SIGNS[si];
const lord = SIGN_LORDS[sign];
const lordPlanet = planets[lord];
const occupants = Object.entries(planets).filter(([_,p]) => p.house === h && !p.error).map(([n,p]) => ({name:n,...p}));
const aspecting = [];
for (const [pn, pp] of Object.entries(planets)) {
if (pp.error) continue;
for (const offset of (PLANET_ASPECTS[pn]||[7])) {
if (((pp.house-1+offset)%12)+1 === h) aspecting.push({name:pn, offset});
}
}
houses.push({ house:h, sign, sign_cn: SIGNS_CN[sign], lord, lord_cn: PLANET_CN[lord]||lord, lordHouse: lordPlanet?lordPlanet.house:null, occupants, aspecting });
}
return houses;
}
// ============================================================================
// 工具
// ============================================================================
function fmtDate(d) {
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
}
export function searchCities(query) {
if (!query || query.length < 1) return [];
const fullQ = query.toLowerCase();
// 拆分关键词:空格/逗号/中文逗号分隔
const words = fullQ.split(/[\s,;]+/).filter(w => w.length > 0);
return CITIES.filter(c => {
const nameLow = c.name.toLowerCase();
const enLow = (c.en || '').toLowerCase();
const combined = nameLow + ' ' + enLow;
// 1. 整体包含(如输入"REDACTED_PLACE"直接匹配)
if (nameLow.includes(fullQ) || enLow.includes(fullQ)) return true;
// 2. 拆词 AND 匹配(如"中国 REDACTED_PLACE"→每个词都须命中)
if (words.length > 1 && words.every(w => combined.includes(w))) return true;
// 3. 模糊匹配:城市名包含输入的子串,或输入包含城市名
// 支持"中国REDACTED_PLACE"匹配"REDACTED_PLACE"、"REDACTED_PLACE市"匹配"REDACTED_PLACE"等
if (nameLow.length >= 2 && (fullQ.includes(nameLow) || nameLow.includes(fullQ))) return true;
if (enLow.length >= 2 && (fullQ.includes(enLow) || enLow.includes(fullQ))) return true;
return false;
}).slice(0, 10);
}