跳转到内容

模組:MoonXiu

维基百科,自由的百科全书

require('Module:Module wikitext')._addText('{{D|G3|G21|见[[U:12З4567/Wenlongtian调查]]}}')
local p = {}

-- ================= 宿度体系数据 =================
local SYSTEMS = {
    huainan = {
        name = "淮南子·黄道宿度",
        origin = 0,
        xiu = {
            {"角",12},{"亢",9},{"氐",15},{"房",5},{"心",5},{"尾",18},{"箕",11},
            {"斗",25},{"牛",7},{"女",11},{"虚",10},{"危",16},{"室",17},{"壁",9},
            {"奎",16},{"娄",12},{"胃",14},{"昴",11},{"毕",16},{"觜",2},{"参",9},
            {"井",33},{"鬼",4},{"柳",15},{"星",7},{"张",18},{"翼",18},{"轸",17}
        }
    },
    dayan_ecl = {
        name = "大衍历·黄道宿度",
        origin = 0,
        xiu = {
            {"角",12.5},{"亢",9.5},{"氐",16},{"房",5.5},{"心",5.5},{"尾",18.5},{"箕",10.5},
            {"斗",24},{"牛",7.5},{"女",11.5},{"虚",9.5},{"危",16.5},{"室",17},{"壁",9.5},
            {"奎",17},{"娄",12.5},{"胃",14.5},{"昴",10.5},{"毕",16.5},{"觜",1.5},{"参",9.5},
            {"井",32},{"鬼",3.5},{"柳",14.5},{"星",7.5},{"张",17.5},{"翼",18.5},{"轸",16.5}
        }
    },
    dayan_equ = {
        name = "大衍历·赤道宿度",
        origin = 0,
        xiu = {
            {"角",12},{"亢",9},{"氐",15},{"房",5},{"心",5},{"尾",18},{"箕",11},
            {"斗",25},{"牛",7},{"女",11},{"虚",10},{"危",16},{"室",17},{"壁",9},
            {"奎",16},{"娄",12},{"胃",14},{"昴",11},{"毕",16},{"觜",2},{"参",9},
            {"井",33},{"鬼",4},{"柳",15},{"星",7},{"张",18},{"翼",18},{"轸",17}
        }
    },
    modern = {
        name = "现代真黄道宿界(含岁差)",
        origin = -28.0,
        xiu = {
            {"角",11.3},{"亢",9.0},{"氐",14.4},{"房",5.2},{"心",6.1},{"尾",19.0},{"箕",10.2},
            {"斗",24.0},{"牛",7.8},{"女",11.0},{"虚",9.5},{"危",16.2},{"室",17.0},{"壁",8.8},
            {"奎",15.5},{"娄",11.8},{"胃",13.5},{"昴",10.2},{"毕",15.8},{"觜",1.8},{"参",9.2},
            {"井",31.5},{"鬼",3.8},{"柳",14.2},{"星",7.2},{"张",17.2},{"翼",17.8},{"轸",16.8}
        }
    }
}

-- ================= 工具函数 =================
local function norm360(x)
    return x % 360
end

local function rad(d) return d * math.pi / 180 end
local function deg(r) return r * 180 / math.pi end

local function findXiu(pos, sys)
    local acc = 0
    for i, x in ipairs(sys.xiu) do
        local w = x[2]
        if pos >= acc and pos < acc + w then
            return x[1], acc, w, i
        end
        acc = acc + w
    end
    local n = #sys.xiu
    local x = sys.xiu[n]
    return x[1], 365.25 - x[2], x[2], n
end

-- ================= 天文算法 (Meeus) =================
local function toJD(y, m, d, h)
    if not y or not m or not d then error("年月日参数不能为空") end
    local a = math.floor((14 - m) / 12)
    local y1 = y + 4800 - a
    local m1 = m + 12 * a - 3
    return d + h/24 + math.floor((153 * m1 + 2) / 5) + 365 * y1 + math.floor(y1 / 4) - 32045.5
end

local function moonEcliptic(jdTT)
    local T = (jdTT - 2451545.0) / 36525.0
    local Lp = 218.3164477 + 481267.88123421 * T - 0.0015786 * T * T
    local D  = 297.8501921 + 445267.1114034 * T - 0.0018819 * T * T
    local M  = 357.5291092 + 35999.0502909 * T - 0.0001536 * T * T
    local Mp = 134.9633964 + 477198.8675055 * T + 0.0087414 * T * T
    local F  = 93.2720950 + 483202.0175233 * T - 0.0036539 * T * T
    local E  = 1 - 0.002516 * T - 0.0000074 * T * T

    local Lp_r, D_r, M_r, Mp_r, F_r = rad(norm360(Lp)), rad(norm360(D)), rad(norm360(M)), rad(norm360(Mp)), rad(norm360(F))

    local sum = 0
    sum = sum + 6288774 * math.sin(Mp_r)
    sum = sum + 1274027 * math.sin(2*D_r - Mp_r)
    sum = sum + 658314 * math.sin(2*D_r)
    sum = sum + 213618 * math.sin(2*Mp_r)
    sum = sum - 185116 * E * math.sin(M_r)
    sum = sum - 114332 * math.sin(2*F_r)
    sum = sum + 58793 * math.sin(2*D_r - 2*Mp_r)
    sum = sum + 57066 * math.sin(2*D_r - M_r - Mp_r)
    local A1 = rad(119.75 + 131.849 * T)
    local A2 = rad(53.09 + 479264.290 * T)
    sum = sum + 3958 * math.sin(A1)
    sum = sum + 1962 * math.sin(Lp_r - F_r)
    sum = sum + 318 * math.sin(A2)

    local lon = norm360(Lp + sum / 1000000)

    local lat_sum = 0
    lat_sum = lat_sum + 5128122 * math.sin(F_r)
    lat_sum = lat_sum + 280602 * math.sin(Mp_r + F_r)
    lat_sum = lat_sum + 277693 * math.sin(Mp_r - F_r)
    local lat = lat_sum / 1000000

    local dist = 385000 - 20905 * math.cos(Mp_r)

    return lon, lat, dist, {Lp=norm360(Lp), D=norm360(D), M=norm360(M), Mp=norm360(Mp), F=norm360(F), E=E}
end

-- ============== 新增:太阳黄经(简化) ==============
local function sunLongitude(jdTT)
    local T = (jdTT - 2451545.0) / 36525.0
    local M = 357.5291092 + 35999.0502909 * T - 0.0001536 * T * T
    local Mr = rad(norm360(M))
    local C = 1.9148 * math.sin(Mr) + 0.0200 * math.sin(2*Mr) + 0.0003 * math.sin(3*Mr)
    local lambda = 280.46646 + 36000.76983 * T + C
    return norm360(lambda)
end

-- ============== 新增:月相计算 ==============
local function moonPhase(jdTT)
    local moon_lon = moonEcliptic(jdTT)  -- 第一个返回值就是月亮黄经
    local sun_lon = sunLongitude(jdTT)
    local delta = norm360(moon_lon - sun_lon)  -- 0~360
    local synodic_day = delta / 360 * 29.53058867
    local illum = (1 - math.cos(rad(delta))) / 2

    -- 现代月相名称
    local modern_name
    if delta < 15 then
        modern_name = "朔"
    elseif delta < 50 then
        modern_name = "朏"
    elseif delta < 85 then
        modern_name = "上弦前"
    elseif delta < 105 then
        modern_name = "上弦"
    elseif delta < 165 then
        modern_name = "望前"
    elseif delta < 195 then
        modern_name = "望"
    elseif delta < 240 then
        modern_name = "既望"
    elseif delta < 265 then
        modern_name = "下弦前"
    elseif delta < 285 then
        modern_name = "下弦"
    elseif delta < 345 then
        modern_name = "下弦后"
    else
        modern_name = "晦"
    end

    -- 西周月相(四分说 + 定点说)
    local zhou_main, zhou_detail
    if synodic_day < 7.5 then
        zhou_main = "初吉"
        if synodic_day < 1.5 then
            zhou_detail = "朔"
        elseif synodic_day < 3 then
            zhou_detail = "哉生霸/朏"
        else
            zhou_detail = "初吉(余)"
        end
    elseif synodic_day < 15 then
        zhou_main = "既生霸"
        if synodic_day < 9 then
            zhou_detail = "上弦"
        elseif synodic_day < 13 then
            zhou_detail = "既生霸"
        else
            zhou_detail = "旁生霸"
        end
    elseif synodic_day < 22.5 then
        zhou_main = "既望"
        if synodic_day < 16.5 then
            zhou_detail = "望"
        elseif synodic_day < 18 then
            zhou_detail = "既望"
        else
            zhou_detail = "既望(余)"
        end
    else
        zhou_main = "既死霸"
        if synodic_day < 24 then
            zhou_detail = "下弦"
        elseif synodic_day < 27 then
            zhou_detail = "既死霸"
        else
            zhou_detail = "晦/旁死霸"
        end
    end

    return {
        phase_angle = delta,
        illum_frac = illum,
        synodic_day = synodic_day,
        phase_name_modern = modern_name,
        phase_name_zhou = zhou_main,
        phase_name_zhou_detail = zhou_detail
    }
end

local function nutation(jdTT)
    local T = (jdTT - 2451545.0) / 36525.0
    local L = rad(280.4665 + 36000.7698 * T)
    local Lp = rad(218.3165 + 481267.8813 * T)
    local omega = rad(125.04452 - 1934.136261 * T)
    local dpsi = -17.20 * math.sin(omega) - 1.32 * math.sin(2*L) - 0.23 * math.sin(2*Lp) + 0.21 * math.sin(2*omega)
    local deps = 9.20 * math.cos(omega) + 0.57 * math.cos(2*L) + 0.10 * math.cos(2*Lp) - 0.09 * math.cos(2*omega)
    return dpsi, deps
end

local function meanObliquity(jdTT)
    local T = (jdTT - 2451545.0) / 36525.0
    return 23.43929111 - 0.013004167 * T - 0.000000164 * T*T + 0.000000503 * T*T*T
end

local function ecl2equ(lon, lat, eps)
    local lon_r, lat_r, eps_r = rad(lon), rad(lat), rad(eps)
    local ra = math.atan2(math.sin(lon_r) * math.cos(eps_r) - math.tan(lat_r) * math.sin(eps_r), math.cos(lon_r))
    local dec = math.asin(math.sin(lat_r) * math.cos(eps_r) + math.cos(lat_r) * math.sin(eps_r) * math.sin(lon_r))
    return norm360(deg(ra)), deg(dec)
end

-- ================= 月离九道 / 月将 =================
local function moonPath(lat, lon)
    if lat > 0 then
        if lon > 0 and lon < 90 then return "青道" end
        if lon >= 90 and lon < 180 then return "朱道" end
        if lon >= 180 and lon < 270 then return "白道" end
        return "黑道"
    else
        if lon > 0 and lon < 90 then return "黑道" end
        if lon >= 90 and lon < 180 then return "白道" end
        if lon >= 180 and lon < 270 then return "青道" end
        return "朱道"
    end
end

local JIANG = {"子","丑","寅","卯","辰","巳","午","未","申","酉","戌","亥"}
local function moonJiang(ra)
    local idx = math.floor((norm360(ra) + 15) / 30) + 1
    return JIANG[idx]
end

-- ================= 主计算 =================
local function compute(args)
    local y = tonumber(args.year)   or os.date("*t").year
    local m = tonumber(args.month)  or 1
    local d = tonumber(args.day)    or 1
    local hh = tonumber(args.hour)  or 0
    local sysKey = args.system or "huainan"
    local sys = SYSTEMS[sysKey] or SYSTEMS.huainan

    local jd = toJD(y, m, d, hh)
    local jdTT = jd + 69.184/86400

    local lon0, lat0, dist0, aux = moonEcliptic(jdTT)
    local dp, de = nutation(jdTT)
    local epsMean = meanObliquity(jdTT)
    local epsTrue = epsMean + de/3600

    local lon = norm360(lon0 + dp/3600)
    local lat = lat0

    local ra, dec = ecl2equ(lon, lat, epsTrue)

    local pos = norm360(lon - sys.origin)
    local star, startDeg, width, idx = findXiu(pos, sys)
    local inDeg = pos - startDeg

    local quji = 90 - dec

    local parallax = 8.794 / (dist0/6378.137)

    local path = moonPath(lat, lon)
    local jiang = moonJiang(ra)

    -- =========== 新增:月相 ===========
    local ph = moonPhase(jdTT)

    return {
        time_str = string.format("%04d-%02d-%02d %02d:00 UTC+8", y, m, d, hh),
        system_key = sysKey,
        system_name = sys.name,
        jd = jd,
        jd_tt = jdTT,
        T = (jdTT - 2451545.0)/36525.0,
        Lp = aux.Lp, D = aux.D, M = aux.M, Mp = aux.Mp, F = aux.F, E = aux.E,
        lon_mean = lon0, lat_mean = lat0,
        nut_dpsi = dp, nut_deps = de,
        eps_mean = epsMean, eps_true = epsTrue,
        lon = lon, lat = lat,
        ra = ra/15,
        ra_deg = ra,
        dec = dec,
        star = star,
        star_index = idx,
        xiu_start = startDeg,
        xiu_width = width,
        in_deg = inDeg,
        quji = quji,
        path = path,
        obliq_angle = 5.1454,
        jiang = jiang,
        dist_km = dist0,
        parallax_sec = parallax,
        -- 月相字段
        phase_angle = ph.phase_angle,
        illum_frac = ph.illum_frac,
        synodic_day = ph.synodic_day,
        phase_name_modern = ph.phase_name_modern,
        phase_name_zhou = ph.phase_name_zhou,
        phase_name_zhou_detail = ph.phase_name_zhou_detail
    }
end

-- ================= 信息框渲染 =================
local function row(label, val, bg)
    bg = bg or "#ffffff"
    return string.format(
        '<div style="display:flex;border-bottom:1px solid #eaecf0;background:%s">'..
        '<div style="width:96px;flex:none;padding:6px 8px;font-weight:bold;background:#f1f4f6;border-right:1px solid #eaecf0">%s</div>'..
        '<div style="padding:6px 8px;flex:1">%s</div></div>', bg, label, val)
end

local function fmt(x, n) return string.format("%."..(n or 4).."f", x) end

local function renderFullBox(d)
    local out = {}
    table.insert(out, '<div style="width:300px;float:right;clear:right;margin:0 0 1em 1em;'..
        'border:1px solid #a2a9b1;border-radius:10px;overflow:hidden;font-family:sans-serif;'..
        'box-shadow:0 2px 6px rgba(0,0,0,.12);background:#fff;font-size:13px;line-height:1.5">')

    table.insert(out, string.format(
        '<div style="background:linear-gradient(90deg,#4a90d9,#6ab0ff);color:#fff;padding:10px 12px;'..
        'font-weight:bold;text-align:center;font-size:15px">月宿推算 · %s</div>', d.time_str))

    table.insert(out, string.format(
        '<div style="padding:12px;text-align:center;background:#f8fbff;border-bottom:1px solid #eaecf0">'..
        '<div style="font-size:18px;font-weight:bold;color:#1a3c6e">入【%s】宿</div>'..
        '<div style="margin-top:4px">入宿距度 <b>%.2f°</b> · 去极度 <b>%.2f°</b></div>'..
        '<div style="font-size:11px;color:#667">距星 %s · 体系:%s</div></div>',
        d.star, d.in_deg, d.quji, d.star, d.system_name))

    table.insert(out, row("计算参数",
        string.format("历元 J2000 · 体系 %s<br/>JD %.5f · TT %.5f<br/>T=%.6f",
        d.system_name, d.jd, d.jd_tt, d.T), "#fafcff"))

    table.insert(out, row("岁差/章动",
        string.format("平黄赤交角 %.4f°<br/>章动 Δψ=%.3f″ Δε=%.3f″<br/>真黄赤交角 %.4f°",
        d.eps_mean, d.nut_dpsi, d.nut_deps, d.eps_true), "#f6f8fa"))

    table.insert(out, row("月亮平坐标",
        string.format("L′=%.4f° M′=%.4f°<br/>D=%.4f° M=%.4f° F=%.4f°<br/>E=%.6f",
        d.Lp, d.Mp, d.D, d.M, d.F, d.E), "#fafcff"))

    table.insert(out, row("月亮真坐标",
        string.format("黄经 %.4f° · 黄纬 %.4f°<br/>赤经 %.4fʰ · 赤纬 %.4f°",
        d.lon, d.lat, d.ra, d.dec), "#f6f8fa"))

    table.insert(out, row("宿度判定",
        string.format("第%d宿 %s<br/>宿界起点 %.2f° 宽 %.2f°<br/>入宿距度 %.3f°",
        d.star_index, d.star, d.xiu_start, d.xiu_width, d.in_deg), "#fafcff"))

    table.insert(out, row("月离九道",
        string.format("通道:%s<br/>黄白交角 %.4f°<br/>月将:%s",
        d.path, d.obliq_angle, d.jiang), "#f6f8fa"))

    -- =========== 新增:月相行 ===========
    table.insert(out, row("月相",
        string.format("相位角 %.2f° · 照明率 %.1f%%<br/>距朔 %.2f 天<br/>现代:%s<br/>西周(四分说):%s<br/>细分:%s",
        d.phase_angle, d.illum_frac*100, d.synodic_day,
        d.phase_name_modern, d.phase_name_zhou, d.phase_name_zhou_detail), "#fafcff"))

    table.insert(out, row("距离与视差",
        string.format("地月距 %.1f km<br/>视差 %.2f″", d.dist_km, d.parallax_sec), "#f6f8fa"))

    table.insert(out, '</div>')
    return table.concat(out)
end

-- ================= 公开接口 =================
function p.moon(frame)
    local d = compute(frame.args)
    if frame.args.format == "fullbox" then
        return renderFullBox(d)
    elseif frame.args.format == "short" then
        local phase = d.phase_name_modern or "—"
        local synodic = d.synodic_day or 0
        return string.format(
            "月亮入【%s】宿,距度 %.2f°,去极度 %.2f°,%s,距朔 %.2f 天",
            d.star, d.in_deg, d.quji, phase, synodic
    )
    else
        return string.format("入【%s】宿 %.2f° / 去极 %.2f°", d.star, d.in_deg, d.quji)
    end
end

function p.get(frame)
    local d = compute(frame.args)
    local key = frame.args.param or frame.args[1]
    if key and d[key] ~= nil then
        if type(d[key]) == "number" then
            return fmt(d[key], 5)
        end
        return tostring(d[key])
    end
    return "未知参数:" .. tostring(key)
end

function p.getJD(frame)
    local y = tonumber(frame.args.year)  or os.date("*t").year
    local m = tonumber(frame.args.month) or 1
    local d = tonumber(frame.args.day)   or 1
    local hh = tonumber(frame.args.hour) or 0
    return string.format("%.5f", toJD(y,m,d,hh))
end

return p