スマート望遠鏡 Seestar で任意の天体 (RA, Dec) を登録するさい、座標が2000年点ではなく現在分点 (JNow) になっています。この仕様は(2025年時点において)、仮に2000年分点のまま天体登録をすると、天体の位置によっては、RA で数分角、Decで数分~数十分角のズレが導入時に生じます。新星などの新天体を観測するさい、座標の変換をしていたほうが(視野中央に導入されるため)、新天体の同定が楽チンなので、Seestarユーザーが少しでもハッピーになればと思い、Webアプリを作ってみた次第 (HTML + CSS + JavaScript)。Seestar で天体導入するには十分な計算結果が得られていると思われる。
(2025.09.18 記)
J2000.0 → 現在分点変換
入力(J2000): —
日時(UT): —
RA (現在分点): —
Dec (現在分点): —
※日時はブラウザより自動取得しています。
※計算コードの原型は ChatGPT-5 で作成しています。
※あくまで自己責任でご利用ください。
コードは以下のとおり:
<div id="precessor-widget" style="max-width:640px;padding:1rem;border:1px solid #ccc;border-radius:8px;font-family:system-ui,Segoe UI,Arial;">
<h3 style="margin-top:0">J2000.0 → 現在分点変換</h3>
<div style="display:flex;gap:.5rem;flex-wrap:wrap">
<label style="flex:1 1 280px">
RA (J2000):<br>
<input id="inRA" type="text" value="" placeholder="hh:mm:ss.s または hh mm ss.s" style="width:100%;padding:.5rem">
</label>
<label style="flex:1 1 280px">
Dec (J2000):<br>
<input id="inDec" type="text" value="" placeholder="±dd:mm:ss.s または ±dd mm ss.s" style="width:100%;padding:.5rem">
</label>
</div>
<button id="doConvert" style="margin-top:1rem;padding:.6rem 1rem;border:0;border-radius:6px;background:#1a73e8;color:#fff;cursor:pointer">
変換(J2000 → 現在分点)
</button>
<div id="out" style="margin-top:1rem;background:#f8f9fa;border:1px solid #e5e7eb;border-radius:6px;padding:.75rem">
<div><strong>入力(J2000):</strong> <span id="echoIn">—</span></div>
<div><strong>日時(UT):</strong> <span id="echoDate">—</span></div>
<hr style="border:none;border-top:1px solid #e5e7eb;margin:.6rem 0">
<div><strong>RA (現在分点):</strong> <span id="outRA" style="color:red">—</span></div>
<div><strong>Dec (現在分点):</strong> <span id="outDec" style="color:red">—</span></div>
</div>
</div>
<script>
// ========= 入力パーサ(: または 半角スペース区切りを許容) =========
function splitTokens(s){ return String(s).trim().replace(/\s+/g,' ').split(/[:\s]+/); }
function hmsToHoursFlexible(s){
const tok = splitTokens(s);
if(tok.length < 2) throw new Error('RAは hh:mm:ss もしくは hh mm ss 形式で入力してください');
const hh = parseFloat(tok[0]), mm = parseFloat(tok[1] ?? '0'), ss = parseFloat(tok[2] ?? '0');
if([hh,mm,ss].some(Number.isNaN)) throw new Error('RAの数値が不正です');
return Math.sign(hh)*Math.abs(hh) + mm/60 + ss/3600;
}
function dmsToDegFlexible(s){
const str = String(s).trim();
const sign = str.startsWith('-') ? -1 : 1;
const core = str.replace(/^[-+]/,'');
const tok = splitTokens(core);
if(tok.length < 2) throw new Error('Decは dd:mm:ss もしくは dd mm ss 形式で入力してください');
const dd = Math.abs(parseFloat(tok[0])), mm = parseFloat(tok[1] ?? '0'), ss = parseFloat(tok[2] ?? '0');
if([dd,mm,ss].some(Number.isNaN)) throw new Error('Decの数値が不正です');
return sign*(dd + mm/60 + ss/3600);
}
// ========= 丸め&表示(秒は整数。繰り上がり対応) =========
function formatRA_hms_intSeconds(raHours){
let totalSec = Math.round((((raHours%24)+24)%24) * 3600);
if (totalSec === 24*3600) totalSec = 0;
const hh = Math.floor(totalSec/3600); totalSec -= hh*3600;
const mm = Math.floor(totalSec/60); const ss = totalSec - mm*60;
return `${String(hh).padStart(2,'0')}:${String(mm).padStart(2,'0')}:${String(ss).padStart(2,'0')}`;
}
function formatDec_dms_intSeconds(decDeg){
const sign = decDeg < 0 ? '-' : '+';
let totalSec = Math.round(Math.abs(decDeg)*3600);
if (totalSec > 90*3600) totalSec = 90*3600; // 安全策
const dd = Math.floor(totalSec/3600); totalSec -= dd*3600;
const mm = Math.floor(totalSec/60); const ss = totalSec - mm*60;
return `${sign}${String(dd).padStart(2,'0')}:${String(mm).padStart(2,'0')}:${String(ss).padStart(2,'0')}`;
}
// ========= 角度・ユーティリティ =========
const DEG2RAD = Math.PI/180;
const RAD2DEG = 180/Math.PI;
function toRadians(deg){ return deg*DEG2RAD; }
function toDegrees(rad){ return rad*RAD2DEG; }
// ========= 時刻:JD(UTC) → JD(TT) 近似 =========
function jdTTfromUTC(dateObj){
// 2025年の近似:TT-UTC ≈ 69.184 s(TAI-UTC=37s; TT=TAI+32.184)
const JD_UNIX_EPOCH = 2440587.5;
const jdUTC = JD_UNIX_EPOCH + dateObj.getTime()/86400000;
return jdUTC + 69.184/86400;
}
// ========= 歳差(IAU 1976/2000 近似) =========
function precessionAngles(t){
const zeta = ((2306.2181+1.39656*t-0.000139*t*t)*t+(0.30188-0.000344*t)*t*t+0.017998*t*t*t);
const z = ((2306.2181+1.39656*t-0.000139*t*t)*t+(1.09468+0.000066*t)*t*t+0.018203*t*t*t);
const theta= ((2004.3109-0.85330*t-0.000217*t*t)*t-(0.42665+0.000217*t)*t*t-0.041833*t*t*t);
const as2r = (Math.PI/180)/3600;
return {zeta:zeta*as2r, z:z*as2r, theta:theta*as2r};
}
function R1(a){ const s=Math.sin(a), c=Math.cos(a); return [[1,0,0],[0,c,s],[0,-s,c]]; }
function R2(a){ const s=Math.sin(a), c=Math.cos(a); return [[c,0,-s],[0,1,0],[s,0,c]]; }
function R3(a){ const s=Math.sin(a), c=Math.cos(a); return [[c,s,0],[-s,c,0],[0,0,1]]; }
function matMul(A,B){ const C=[[0,0,0],[0,0,0],[0,0,0]]; for(let i=0;i<3;i++)for(let j=0;j<3;j++)for(let k=0;k<3;k++)C[i][j]+=A[i][k]*B[k][j]; return C; }
function matVec(A,v){ return [A[0][0]*v[0]+A[0][1]*v[1]+A[0][2]*v[2], A[1][0]*v[0]+A[1][1]*v[1]+A[1][2]*v[2], A[2][0]*v[0]+A[2][1]*v[1]+A[2][2]*v[2]]; }
// ========= 平均黄道傾斜(ε̄) =========
function meanObliquity(t){
// arcseconds
const eps = 84381.448 - 46.8150*t - 0.00059*t*t + 0.001813*t*t*t;
return eps * (Math.PI/180) / 3600; // radians
}
// ========= 章動(簡略版:Meeusの近似;主成分) =========
// Δψ, Δε をラジアンで返す(小さくても回転なのでラジアンでOK)
function nutationMeeusApprox(jdTT){
const T = (jdTT - 2451545.0)/36525.0; // Julian centuries (TT)
// 基本引数(度) - Meeus (1998) Chapter 22 式
const L = 280.4665 + 36000.7698*T; // 太陽の平均黄経(≒黄経)[deg]
const Ls = 357.5291 + 35999.0503*T; // 太陽の平均近点離角(M_sun)[deg]
const Lm = 134.96298 + 477198.867398*T + 0.0086972*T*T; // 月の平均近点離角(M_moon)[deg] 近似
const D = 297.85036 + 445267.111480*T - 0.0019142*T*T; // 月の平均伸開角 [deg]
const Om = 125.04452 - 1934.136261*T + 0.0020708*T*T; // 月昇交点黄経 [deg]
// 角度をラジアンへ
const Lr = (L%360)*DEG2RAD;
const Lsr = (Ls%360)*DEG2RAD;
const Lmr = (Lm%360)*DEG2RAD;
const Dr = (D%360)*DEG2RAD;
const Omr = (Om%360)*DEG2RAD;
// 簡略式(単位:arcsec)
// Δψ ≈ -17.20*sinΩ - 1.32*sin(2L_sun) - 0.23*sin(2L_moon) + 0.21*sin(2Ω)
// Δε ≈ +9.20*cosΩ + 0.57*cos(2L_sun) + 0.10*cos(2L_moon) - 0.09*cos(2Ω)
const dpsi_as = -17.20*Math.sin(Omr)
- 1.32*Math.sin(2*Lsr)
- 0.23*Math.sin(2*Lmr)
+ 0.21*Math.sin(2*Omr);
const deps_as = +9.20*Math.cos(Omr)
+ 0.57*Math.cos(2*Lsr)
+ 0.10*Math.cos(2*Lmr)
- 0.09*Math.cos(2*Omr);
const as2r = (Math.PI/180)/3600;
return { dpsi: dpsi_as*as2r, deps: deps_as*as2r };
}
// ========= メイン:J2000 → 真分点(歳差+章動) =========
function j2000ToTrueOfDate(raHours, decDeg, jdTT){
const t = (jdTT - 2451545.0)/36525.0;
// 歳差:J2000 → 平均赤道・平均分点(of date)
const {zeta,z,theta} = precessionAngles(t);
const P = matMul( matMul(R3(-z), R2(theta)), R3(-zeta) );
// 章動:平均 → 真(of date)
const eps_bar = meanObliquity(t); // 平均黄道傾斜 ε̄
const {dpsi,deps} = nutationMeeusApprox(jdTT); // Δψ, Δε
const N = matMul( R1(-(eps_bar+deps)), matMul( R3(-dpsi), R1(eps_bar) ) );
// 合成回転:J2000 → 真分点(of date)
const X = matMul(N, P);
// ベクトル化→回転→(α,δ)
const a = toRadians(raHours*15), d = toRadians(decDeg);
const r0 = [Math.cos(d)*Math.cos(a), Math.cos(d)*Math.sin(a), Math.sin(d)];
const r = matVec(X, r0);
const ra = Math.atan2(r[1], r[0]);
const dec = Math.asin(r[2]);
let raH = toDegrees(ra)/15; if(raH<0) raH += 24;
return { raHours: raH, decDeg: toDegrees(dec) };
}
// ========= UI結線 =========
(function(){
const $ = id => document.getElementById(id);
$('doConvert').addEventListener('click', ()=>{
try{
const raH = hmsToHoursFlexible($('inRA').value);
const decD = dmsToDegFlexible($('inDec').value);
const now = new Date(); // ブラウザ現在(UTC)
const jdTT = jdTTfromUTC(now); // TT 近似
const res = j2000ToTrueOfDate(raH, decD, jdTT);
$('echoIn').textContent = `RA ${$('inRA').value}, Dec ${$('inDec').value}`;
$('echoDate').textContent = `${now.toISOString().replace('T',' ').replace('Z','')} (JD_TT ≈ ${jdTT.toFixed(5)})`;
$('outRA').textContent = formatRA_hms_intSeconds(res.raHours);
$('outDec').textContent = formatDec_dms_intSeconds(res.decDeg);
}catch(e){
alert(e.message||e);
}
});
})();
</script>