тест
<!doctype html>
<html lang="bg">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Asteroid Ephemeris (Offline MVP)</title>
<style>
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; margin: 0; padding: 16px; }
.wrap { max-width: 900px; margin: 0 auto; }
h1 { font-size: 18px; margin: 0 0 12px; }
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
label { display: grid; gap: 6px; font-size: 13px; }
input { padding: 10px; border: 1px solid #ccc; border-radius: 10px; font-size: 14px; }
button { width: 100%; padding: 12px; border: 0; border-radius: 12px; font-weight: 600; font-size: 15px; cursor: pointer; }
.card { margin-top: 12px; padding: 12px; border: 1px solid #e3e3e3; border-radius: 12px; }
pre { margin: 0; white-space: pre-wrap; word-break: break-word; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.note { margin-top: 12px; font-size: 12px; color: #555; line-height: 1.35; }
.row { display: grid; grid-template-columns: 1fr; gap: 10px; margin-top: 10px; }
@media (max-width: 680px) { .grid { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<div class="wrap">
<h1>Asteroid Ephemeris (Offline MVP) — елементи → RA/Dec</h1>
<div class="grid">
<label>a (AU)
<input id="a" value="2.35" inputmode="decimal">
</label>
<label>e
<input id="e" value="0.12" inputmode="decimal">
</label>
<label>i (deg)
<input id="i" value="5.3" inputmode="decimal">
</label>
<label>Ω RAAN (deg)
<input id="raan" value="80.0" inputmode="decimal">
</label>
<label>ω (deg)
<input id="argp" value="150.0" inputmode="decimal">
</label>
<label>M0 (deg) at epoch
<input id="m0" value="10.0" inputmode="decimal">
</label>
</div>
<div class="row">
<label>Epoch UTC (ISO) — напр. 2025-01-01T00:00:00
<input id="epoch" value="2025-01-01T00:00:00">
</label>
<label>Obs time UTC (ISO) — напр. 2025-12-29T21:00:00
<input id="obs" value="2025-12-29T21:00:00">
</label>
<button id="calc">Calculate</button>
</div>
<div class="card" id="out" style="display:none">
<pre id="outText"></pre>
</div>
<div class="card" id="err" style="display:none;border-color:#ffcccc">
<pre id="errText" style="color:#a40000"></pre>
</div>
<div class="note">
Бележка: това е офлайн MVP (геоцентрично, без светлинно време и без JPL DE ефемериди за Земята).
За ориентир/насочване е ок. Ако искаш, мога да добавя Alt/Az от GPS (в браузър) и/или по-точна Земя.
</div>
</div>
<script>
/** ---------- math helpers ---------- **/
const DEG2RAD = Math.PI / 180;
const RAD2DEG = 180 / Math.PI;
function norm360(d) {
d = d % 360;
return d < 0 ? d + 360 : d;
}
function sinD(d){ return Math.sin(d*DEG2RAD); }
function cosD(d){ return Math.cos(d*DEG2RAD); }
function solveKeplerElliptic(M, e) {
// M radians
M = M % (2*Math.PI);
if (M < 0) M += 2*Math.PI;
let E = (e < 0.8) ? M : Math.PI;
for (let k = 0; k < 60; k++) {
const f = E - e*Math.sin(E) - M;
const fp = 1 - e*Math.cos(E);
const dE = -f / fp;
E += dE;
if (Math.abs(dE) < 1e-12) break;
}
return E;
}
function rotZ(a){
const c = Math.cos(a), s = Math.sin(a);
return [
[ c,-s, 0],
[ s, c, 0],
[ 0, 0, 1]
];
}
function rotX(a){
const c = Math.cos(a), s = Math.sin(a);
return [
[1, 0, 0],
[0, c,-s],
[0, s, c]
];
}
function matMul(A,B){
const o = [[0,0,0],[0,0,0],[0,0,0]];
for(let i=0;i<3;i++) for(let j=0;j<3;j++){
let sum=0;
for(let k=0;k<3;k++) sum += A[i][k]*B[k][j];
o[i][j]=sum;
}
return o;
}
function matVec(M,v){
return [
M[0][0]*v[0] + M[0][1]*v[1] + M[0][2]*v[2],
M[1][0]*v[0] + M[1][1]*v[1] + M[1][2]*v[2],
M[2][0]*v[0] + M[2][1]*v[1] + M[2][2]*v[2],
];
}
/** ---------- time: ISO UTC -> Julian Day (UTC-ish) ---------- **/
function isoToJD(iso) {
iso = iso.trim().replace(' ', 'T');
const [dateStr, timeStr] = iso.split('T');
if (!dateStr || !timeStr) throw new Error("Невалиден ISO формат (очаквам YYYY-MM-DDTHH:MM:SS)");
const [Y,M,D] = dateStr.split('-').map(x=>parseInt(x,10));
const parts = timeStr.split(':').map(x=>parseFloat(x));
const hh = parts[0] ?? 0, mm = parts[1] ?? 0, ss = parts[2] ?? 0;
const frac = (hh + mm/60 + ss/3600)/24;
return gregorianToJD(Y,M,D,frac);
}
function gregorianToJD(yIn,mIn,dIn,fracDay){
let y=yIn, m=mIn;
let d = dIn + fracDay;
if (m <= 2) { y -= 1; m += 12; }
const a = Math.floor(y/100);
const b = 2 - a + Math.floor(a/4);
const jd = Math.floor(365.25*(y+4716)) + Math.floor(30.6001*(m+1)) + d + b - 1524.5;
return jd;
}
/** ---------- Earth heliocentric (offline approx) ---------- **/
function earthHelioEquatorialAu(jdUtc){
const n = jdUtc - 2451545.0;
const L = norm360(280.460 + 0.9856474*n);
const g = norm360(357.528 + 0.9856003*n);
const lambda = norm360(L + 1.915*sinD(g) + 0.020*sinD(2*g));
const R = 1.00014 - 0.01671*cosD(g) - 0.00014*cosD(2*g);
// Sun geocentric ecliptic vector (Earth -> Sun)
const xSun = R*cosD(lambda);
const ySun = R*sinD(lambda);
// Earth heliocentric ecliptic = - (Earth->Sun)
const xEcl = -xSun, yEcl = -ySun, zEcl = 0;
const eps = 23.439 - 0.0000004*n; // deg
const xEq = xEcl;
const yEq = yEcl*cosD(eps) - zEcl*sinD(eps);
const zEq = yEcl*sinD(eps) + zEcl*cosD(eps);
return [xEq,yEq,zEq];
}
/** ---------- main ephemeris ---------- **/
function computeRaDec(el, obsIsoUtc){
if (!(el.e >= 0 && el.e < 1)) throw new Error("MVP поддържа само елиптични орбити (0 <= e < 1).");
// Gaussian gravitational constant k (AU^(3/2)/day)
const k = 0.01720209895;
const mu = k*k;
const jd = isoToJD(obsIsoUtc);
const jd0 = isoToJD(el.epochIsoUtc);
const dtDays = jd - jd0;
const a = el.aAu, e = el.e;
const i = el.iDeg*DEG2RAD;
const raan = el.raanDeg*DEG2RAD;
const argp = el.argpDeg*DEG2RAD;
const m0 = el.m0Deg*DEG2RAD;
const n = Math.sqrt(mu/(a*a*a)); // rad/day
const M = m0 + n*dtDays;
const E = solveKeplerElliptic(M,e);
const nu = 2*Math.atan2(Math.sqrt(1+e)*Math.sin(E/2), Math.sqrt(1-e)*Math.cos(E/2));
const r = a*(1 - e*Math.cos(E));
const rPqw = [r*Math.cos(nu), r*Math.sin(nu), 0];
const R = matMul(matMul(rotZ(raan), rotX(i)), rotZ(argp));
const rAstHelioEq = matVec(R, rPqw);
const rEarthHelioEq = earthHelioEquatorialAu(jd);
const rg = [
rAstHelioEq[0] - rEarthHelioEq[0],
rAstHelioEq[1] - rEarthHelioEq[1],
rAstHelioEq[2] - rEarthHelioEq[2],
];
const dist = Math.hypot(rg[0], rg[1], rg[2]);
let ra = Math.atan2(rg[1], rg[0]);
if (ra < 0) ra += 2*Math.PI;
const dec = Math.asin(rg[2]/dist);
return { raDeg: ra*RAD2DEG, decDeg: dec*RAD2DEG, distAu: dist };
}
function formatRaHms(raDeg){
const totalHours = raDeg/15;
const h = Math.floor(totalHours);
const mFloat = (totalHours - h)*60;
const m = Math.floor(mFloat);
const s = (mFloat - m)*60;
return `${String(h).padStart(2,'0')}h ${String(m).padStart(2,'0')}m ${s.toFixed(2).padStart(5,'0')}s`;
}
function formatDecDms(decDeg){
const sign = decDeg >= 0 ? "+" : "-";
const absd = Math.abs(decDeg);
const d = Math.floor(absd);
const mFloat = (absd - d)*60;
const m = Math.floor(mFloat);
const s = (mFloat - m)*60;
return `${sign}${String(d).padStart(2,'0')}° ${String(m).padStart(2,'0')}' ${s.toFixed(2).padStart(5,'0')}"`;
}
/** ---------- UI wiring ---------- **/
const $ = (id)=>document.getElementById(id);
function readNum(id){
const v = $(id).value.trim().replace(',', '.');
const x = Number(v);
if (!Number.isFinite(x)) throw new Error(`Невалидно число в полето: ${id}`);
return x;
}
$("calc").addEventListener("click", () => {
$("err").style.display = "none";
$("out").style.display = "none";
try{
const el = {
aAu: readNum("a"),
e: readNum("e"),
iDeg: readNum("i"),
raanDeg: readNum("raan"),
argpDeg: readNum("argp"),
m0Deg: readNum("m0"),
epochIsoUtc: $("epoch").value.trim()
};
const obs = $("obs").value.trim();
const res = computeRaDec(el, obs);
const text =
`RA: ${res.raDeg.toFixed(6)}° (${formatRaHms(res.raDeg)})\n` +
`Dec: ${res.decDeg.toFixed(6)}° (${formatDecDms(res.decDeg)})\n` +
`Δ: ${res.distAu.toFixed(6)} AU`;
$("outText").textContent = text;
$("out").style.display = "block";
}catch(err){
$("errText").textContent = String(err && err.message ? err.message : err);
$("err").style.display = "block";
}
});
</script>
</body>
</html>
Коментари
Публикуване на коментар