Загрузка данных
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Звёздный глобус Ø1000 мм — 24 лепестка — J2000</title>
<style>
body {
margin: 0;
padding: 20px;
font-family: Arial, sans-serif;
background: #eeeeee;
color: #111;
}
.panel {
max-width: 1100px;
margin: 0 auto 20px;
padding: 20px;
background: white;
border-radius: 8px;
box-sizing: border-box;
}
h1, h2, h3 {
margin-top: 0;
}
button {
padding: 12px 18px;
margin: 5px;
border: 1px solid #333;
background: white;
border-radius: 5px;
cursor: pointer;
font-size: 15px;
}
button:hover {
background: #eeeeee;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
#status {
margin-top: 15px;
padding: 12px;
background: #f5f5f5;
font-family: monospace;
white-space: pre-line;
border-radius: 4px;
}
#preview {
overflow: auto;
text-align: center;
}
#preview svg {
width: 100%;
max-width: 500px;
height: auto;
background: white;
border: 1px solid #aaa;
}
.small {
font-size: 13px;
color: #555;
line-height: 1.5;
}
.stats {
margin-top: 15px;
font-size: 14px;
color: #333;
line-height: 1.7;
}
.ok {
color: #176b2c;
}
.warning {
color: #a15c00;
}
.error {
color: #a00000;
}
.formula {
background: #f5f5f5;
padding: 12px;
border-radius: 5px;
font-family: monospace;
overflow-x: auto;
}
</style>
</head>
<body>
<div class="panel">
<h1>Звёздный глобус Ø1000 мм</h1>
<p>
<b>24 лепестка</b> ·
<b>J2000.0</b> ·
звёзды до <b>5.0m</b>
</p>
<p class="small">
Sinusoidal projection.
Каждый лепесток охватывает 15° прямого восхождения.
Северный полюс находится сверху SVG,
южный — снизу.
Все размеры SVG заданы в миллиметрах.
При печати использовать масштаб 100%.
</p>
<div class="formula">
x = R · cos(Dec) · sin(RA − RA_center)<br>
y = −R · Dec_rad<br>
R = 500 мм
</div>
<button id="downloadAllButton"
onclick="downloadAll()"
disabled>
Скачать 24 SVG
</button>
<button id="downloadZipButton"
onclick="downloadZIP()"
disabled>
Скачать ZIP
</button>
<div id="status">
Загрузка каталога...
</div>
<div id="stats" class="stats"></div>
</div>
<div class="panel">
<h2>Предпросмотр лепестка №1</h2>
<p class="small">
Preview показывает звёзды и границу лепестка.
Красная точка — звезда, находящаяся непосредственно
около границы RA лепестка.
</p>
<div id="preview"></div>
</div>
<script>
/* =========================================================
ОСНОВНЫЕ ПАРАМЕТРЫ
========================================================= */
const DIAMETER_MM = 1000;
const RADIUS_MM = DIAMETER_MM / 2;
const NUMBER_OF_GORES = 24;
const GORE_DEGREES = 360 / NUMBER_OF_GORES;
const HALF_GORE_DEGREES = GORE_DEGREES / 2;
const MAX_MAGNITUDE = 5.0;
/*
* Техническое поле вокруг геометрии.
*/
const MARGIN_MM = 12;
/*
* Обводка лепестка.
*/
const GORE_STROKE = "#999999";
const GORE_STROKE_WIDTH = 0.2;
/*
* Линии контроля.
*/
const SHOW_EQUATOR = false;
const SHOW_POLES = false;
const SHOW_GRID = false;
/*
* Пограничная зона.
* Если звезда находится ближе этого угла
* к границе лепестка, она считается boundary-near.
*/
const BOUNDARY_TOLERANCE_DEG = 0.01;
/*
* Максимальная допустимая ошибка
* обратного преобразования.
*
* Для J2000 координат это только
* математический контроль нашего
* преобразования.
*/
const ROUNDTRIP_TOLERANCE_DEG = 1e-9;
/*
* Каталог Yale Bright Star Catalog.
*/
const CATALOG_URL =
"https://brettonw.github.io/YaleBrightStarCatalog/bsc5.json";
let stars = [];
let catalogLoaded = false;
/* =========================================================
МАТЕМАТИКА
========================================================= */
function degToRad(deg) {
return deg * Math.PI / 180;
}
function radToDeg(rad) {
return rad * 180 / Math.PI;
}
function sleep(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
function fmt(n) {
return Number(Number(n).toFixed(3));
}
function setStatus(text) {
document.getElementById("status").textContent = text;
}
function normalizeRA(ra) {
let value = Number(ra) % 360;
if (value < 0) {
value += 360;
}
/*
* Не допускаем 360.
*/
if (value >= 360) {
value -= 360;
}
return value;
}
/*
* Возвращает угол [-180, +180).
*/
function signedAngle(deg) {
return (
((deg + 180) % 360 + 360) % 360 - 180
);
}
/* =========================================================
PARSE RA
========================================================= */
function parseRA(value) {
if (
value === undefined ||
value === null
) {
return null;
}
if (typeof value === "number") {
if (!Number.isFinite(value)) {
return null;
}
/*
* В BSC числовое RA обычно часы.
*
* Если 0...24 — считаем часами.
*/
if (
value >= 0 &&
value <= 24
) {
return normalizeRA(value * 15);
}
return normalizeRA(value);
}
const text =
String(value)
.trim()
.replace(/h/gi, " ")
.replace(/m/gi, " ")
.replace(/s/gi, " ")
.replace(/:/g, " ")
.replace(/,/g, ".")
.replace(/\s+/g, " ");
if (!text) {
return null;
}
const parts =
text
.split(" ")
.filter(Boolean);
const h = Number(parts[0]);
const m =
parts.length > 1
? Number(parts[1])
: 0;
const s =
parts.length > 2
? Number(parts[2])
: 0;
if (
!Number.isFinite(h) ||
!Number.isFinite(m) ||
!Number.isFinite(s)
) {
return null;
}
if (
h < 0 ||
h > 24 ||
m < 0 ||
m >= 60 ||
s < 0 ||
s >= 60
) {
return null;
}
return normalizeRA(
15 * (
h +
m / 60 +
s / 3600
)
);
}
/* =========================================================
PARSE DEC
========================================================= */
function parseDec(value) {
if (
value === undefined ||
value === null
) {
return null;
}
if (typeof value === "number") {
if (!Number.isFinite(value)) {
return null;
}
if (
value < -90 ||
value > 90
) {
return null;
}
return value;
}
const raw =
String(value)
.trim()
.replace(/−/g, "-")
.replace(/°/g, " ")
.replace(/′/g, " ")
.replace(/″/g, " ")
.replace(/:/g, " ")
.replace(/,/g, ".")
.replace(/\s+/g, " ");
if (!raw) {
return null;
}
const negative =
raw.startsWith("-");
const cleaned =
raw.replace(
/^[+\-]/,
""
);
const parts =
cleaned
.split(" ")
.filter(Boolean);
const d = Number(parts[0]);
const m =
parts.length > 1
? Number(parts[1])
: 0;
const s =
parts.length > 2
? Number(parts[2])
: 0;
if (
!Number.isFinite(d) ||
!Number.isFinite(m) ||
!Number.isFinite(s)
) {
return null;
}
if (
d < 0 ||
d > 90 ||
m < 0 ||
m >= 60 ||
s < 0 ||
s >= 60
) {
return null;
}
let dec =
d +
m / 60 +
s / 3600;
if (negative) {
dec = -dec;
}
if (
dec < -90 ||
dec > 90
) {
return null;
}
return dec;
}
/* =========================================================
GET RA
========================================================= */
function getRA(record) {
if (
record.RA !== undefined &&
record.RA !== null
) {
const ra =
parseRA(record.RA);
if (ra !== null) {
return ra;
}
}
const h =
Number(record.RAh);
const m =
Number(record.RAm);
const s =
Number(record.RAs);
if (
Number.isFinite(h) &&
Number.isFinite(m) &&
Number.isFinite(s)
) {
return normalizeRA(
15 * (
h +
m / 60 +
s / 3600
)
);
}
return null;
}
/* =========================================================
GET DEC
========================================================= */
function getDec(record) {
if (
record.Dec !== undefined &&
record.Dec !== null
) {
const dec =
parseDec(record.Dec);
if (dec !== null) {
return dec;
}
}
const degrees =
Number(record.DEd);
const minutes =
Number(record.DEm);
const seconds =
Number(record.DEs);
if (
!Number.isFinite(degrees) ||
!Number.isFinite(minutes) ||
!Number.isFinite(seconds)
) {
return null;
}
let dec =
Math.abs(degrees) +
Math.abs(minutes) / 60 +
Math.abs(seconds) / 3600;
const sign =
record["DE-"];
if (
sign !== undefined &&
sign !== null
) {
const value =
String(sign)
.trim()
.toUpperCase();
if (
value === "-" ||
value === "−" ||
value === "S"
) {
dec = -dec;
}
}
else if (degrees < 0) {
dec = -dec;
}
return dec;
}
/* =========================================================
LOAD CATALOG
========================================================= */
async function loadCatalog() {
if (catalogLoaded) {
return true;
}
setStatus(
"Загрузка Yale Bright Star Catalog..."
);
try {
const response =
await fetch(
CATALOG_URL,
{
cache: "no-cache"
}
);
if (!response.ok) {
throw new Error(
"HTTP " +
response.status +
" " +
response.statusText
);
}
const data =
await response.json();
if (!Array.isArray(data)) {
throw new Error(
"Каталог имеет неожиданный формат: " +
"ожидался JSON-массив."
);
}
const result = [];
for (
const record of data
) {
const mag =
Number(record.Vmag);
if (!Number.isFinite(mag)) {
continue;
}
if (
mag > MAX_MAGNITUDE
) {
continue;
}
const ra =
getRA(record);
if (ra === null) {
continue;
}
const dec =
getDec(record);
if (dec === null) {
continue;
}
result.push({
ra: normalizeRA(ra),
dec: dec,
mag: mag,
hr:
record.HR ??
null,
name:
record.Common ??
null
});
}
if (!result.length) {
throw new Error(
"После обработки каталога " +
"не осталось звёзд."
);
}
stars = result;
catalogLoaded = true;
setStatus(
"Каталог загружен.\n" +
"Записей в JSON: " +
data.length +
"\n" +
"Звёзд V ≤ " +
MAX_MAGNITUDE.toFixed(1) +
"m: " +
stars.length
);
runFullGeometryCheck();
updateStats();
enableButtons();
renderPreview();
return true;
}
catch (error) {
console.error(
"Ошибка загрузки каталога:",
error
);
setStatus(
"ОШИБКА ЗАГРУЗКИ КАТАЛОГА\n\n" +
error.message +
"\n\n" +
"Если HTML открыт напрямую через file://,\n" +
"запустите его через локальный HTTP-сервер."
);
return false;
}
}
/* =========================================================
GORE CENTER
========================================================= */
function goreCenterRA(index) {
return normalizeRA(
index * GORE_DEGREES
);
}
/* =========================================================
GORE INDEX
========================================================= */
function goreIndexForRA(ra) {
const normalized =
normalizeRA(ra);
/*
* Сдвигаем на половину лепестка,
* чтобы диапазоны стали:
*
* 0: 352.5...7.5
* 1: 7.5...22.5
* ...
*/
const shifted =
normalizeRA(
normalized +
HALF_GORE_DEGREES
);
let index =
Math.floor(
shifted /
GORE_DEGREES
);
if (
index < 0 ||
index >= NUMBER_OF_GORES
) {
index = 0;
}
return index;
}
/* =========================================================
STAR → GORE
========================================================= */
function starsForGore(index) {
return stars.filter(
star =>
goreIndexForRA(star.ra) === index
);
}
/* =========================================================
DISTANCE TO GORE BORDER
========================================================= */
function distanceToGoreBorder(
ra,
index
) {
const center =
goreCenterRA(index);
const delta =
Math.abs(
signedAngle(
ra - center
)
);
return HALF_GORE_DEGREES - delta;
}
/* =========================================================
FORWARD PROJECTION
========================================================= */
/*
* Sinusoidal projection.
*
* lambda = RA offset from gore center.
* phi = declination.
*
* x = R cos(phi) sin(lambda)
* y = -R phi
*
* SVG Y is inverted intentionally:
* north = negative Y.
*/
function projectPoint(
deltaRA,
dec
) {
const lambda =
degToRad(deltaRA);
const phi =
degToRad(dec);
return {
x:
RADIUS_MM *
Math.cos(phi) *
Math.sin(lambda),
y:
-RADIUS_MM *
phi
};
}
/* =========================================================
INVERSE PROJECTION
========================================================= */
function inverseProjectPoint(
x,
y,
centerRA
) {
/*
* phi = -y / R
*/
const phi =
-y /
RADIUS_MM;
/*
* lambda:
*
* x = R cos(phi) sin(lambda)
*
* sin(lambda) =
* x / (R cos(phi))
*/
const cosPhi =
Math.cos(phi);
let ratio =
x /
(
RADIUS_MM *
cosPhi
);
/*
* Защита от микроскопического
* floating point выхода за [-1,1].
*/
ratio =
Math.max(
-1,
Math.min(
1,
ratio
)
);
const lambda =
Math.asin(ratio);
const deltaRA =
radToDeg(lambda);
const dec =
radToDeg(phi);
const ra =
normalizeRA(
centerRA +
deltaRA
);
return {
ra: ra,
dec: dec,
deltaRA: deltaRA
};
}
/* =========================================================
ROUND TRIP CHECK
========================================================= */
function checkRoundTrip(
star,
index
) {
const center =
goreCenterRA(index);
const delta =
signedAngle(
star.ra -
center
);
const p =
projectPoint(
delta,
star.dec
);
const inverse =
inverseProjectPoint(
p.x,
p.y,
center
);
const raError =
Math.abs(
signedAngle(
inverse.ra -
star.ra
)
);
const decError =
Math.abs(
inverse.dec -
star.dec
);
return {
raError,
decError,
ok:
raError <=
ROUNDTRIP_TOLERANCE_DEG &&
decError <=
ROUNDTRIP_TOLERANCE_DEG
};
}
/* =========================================================
GEOMETRY CHECK
========================================================= */
function runFullGeometryCheck() {
let failed = 0;
let boundaryStars = 0;
let maxRAError = 0;
let maxDecError = 0;
let maxXError = 0;
let maxYError = 0;
for (
const star of stars
) {
const index =
goreIndexForRA(star.ra);
const center =
goreCenterRA(index);
const delta =
signedAngle(
star.ra -
center
);
const p =
projectPoint(
delta,
star.dec
);
/*
* Обратная проекция.
*/
const inverse =
inverseProjectPoint(
p.x,
p.y,
center
);
/*
* Ошибки.
*/
const raError =
Math.abs(
signedAngle(
inverse.ra -
star.ra
)
);
const decError =
Math.abs(
inverse.dec -
star.dec
);
maxRAError =
Math.max(
maxRAError,
raError
);
maxDecError =
Math.max(
maxDecError,
decError
);
/*
* Повторная прямая проекция.
*/
const p2 =
projectPoint(
inverse.deltaRA,
inverse.dec
);
maxXError =
Math.max(
maxXError,
Math.abs(
p2.x -
p.x
)
);
maxYError =
Math.max(
maxYError,
Math.abs(
p2.y -
p.y
)
);
/*
* Контроль границы.
*/
const distance =
distanceToGoreBorder(
star.ra,
index
);
if (
distance <=
BOUNDARY_TOLERANCE_DEG
) {
boundaryStars++;
}
/*
* Проверка принадлежности.
*/
if (
Math.abs(delta) >
HALF_GORE_DEGREES +
1e-10
) {
failed++;
}
/*
* Проверка round-trip.
*/
if (
raError >
ROUNDTRIP_TOLERANCE_DEG ||
decError >
ROUNDTRIP_TOLERANCE_DEG
) {
failed++;
}
}
const resultClass =
failed === 0
? "ok"
: "error";
const stats =
document.getElementById(
"stats"
);
stats.innerHTML =
`<span class="${resultClass}">
Геометрия:
${failed === 0 ? "OK" : "ОШИБКА"}
</span><br>
Звёзд: ${stars.length}<br>
Максимальная ошибка RA round-trip:
${maxRAError.toExponential(3)}°<br>
Максимальная ошибка Dec round-trip:
${maxDecError.toExponential(3)}°<br>
Максимальная ошибка X:
${maxXError.toExponential(3)} мм<br>
Максимальная ошибка Y:
${maxYError.toExponential(3)} мм<br>
Звёзд ближе ${BOUNDARY_TOLERANCE_DEG}°
к границе:
<span class="${boundaryStars ? "warning" : "ok"}">
${boundaryStars}
</span>`;
}
/* =========================================================
GORE BOUNDARY
========================================================= */
function goreBoundary() {
const half =
degToRad(
HALF_GORE_DEGREES
);
const points = [];
/*
* Север → юг
* левая граница.
*/
for (
let i = 0;
i <= 720;
i++
) {
const phi =
Math.PI / 2 -
Math.PI * i / 720;
points.push({
x:
RADIUS_MM *
Math.cos(phi) *
Math.sin(-half),
y:
-RADIUS_MM *
phi
});
}
/*
* Юг → север
* правая граница.
*/
for (
let i = 720;
i >= 0;
i--
) {
const phi =
Math.PI / 2 -
Math.PI * i / 720;
points.push({
x:
RADIUS_MM *
Math.cos(phi) *
Math.sin(half),
y:
-RADIUS_MM *
phi
});
}
return points;
}
/* =========================================================
PATH
========================================================= */
function pointsToPath(points) {
if (!points.length) {
return "";
}
let d =
"M " +
fmt(points[0].x) +
" " +
fmt(points[0].y);
for (
let i = 1;
i < points.length;
i++
) {
d +=
" L " +
fmt(points[i].x) +
" " +
fmt(points[i].y);
}
return d + " Z";
}
/* =========================================================
STAR RADIUS
========================================================= */
function starRadius(mag) {
const brightness =
Math.max(
0,
Math.min(
5,
5 - mag
)
);
/*
* Графическая шкала.
*
* Не является физическим
* угловым диаметром звезды.
*/
const originalRadius =
1.0 +
Math.pow(
brightness / 5,
0.65
) * 2.6;
const r =
originalRadius / 6;
return Math.max(
0.15,
Math.min(
0.6,
r
)
);
}
/* =========================================================
STAR METADATA
========================================================= */
function escapeXML(value) {
return String(value)
.replace(/&/g, "&")
.replace(/"/g, """)
.replace(/</g, "<")
.replace(/>/g, ">");
}
/* =========================================================
SVG ONE GORE
========================================================= */
function createGoreSVG(index) {
const starsHere =
starsForGore(index);
const halfWidth =
RADIUS_MM *
Math.sin(
degToRad(
HALF_GORE_DEGREES
)
);
const goreWidth =
halfWidth * 2;
const totalWidth =
goreWidth +
MARGIN_MM * 2;
const totalHeight =
Math.PI *
RADIUS_MM +
MARGIN_MM * 2;
const viewMinX =
-totalWidth / 2;
const viewMinY =
-totalHeight / 2;
const boundary =
goreBoundary();
const boundaryPath =
pointsToPath(boundary);
const northPoleY =
-Math.PI *
RADIUS_MM /
2;
const southPoleY =
Math.PI *
RADIUS_MM /
2;
const raStart =
normalizeRA(
goreCenterRA(index) -
HALF_GORE_DEGREES
);
const raEnd =
normalizeRA(
goreCenterRA(index) +
HALF_GORE_DEGREES
);
let svg = "";
svg +=
`<?xml version="1.0" encoding="UTF-8"?>
<svg
xmlns="http://www.w3.org/2000/svg"
version="1.1"
width="${fmt(totalWidth)}mm"
height="${fmt(totalHeight)}mm"
viewBox="${fmt(viewMinX)} ${fmt(viewMinY)} ${fmt(totalWidth)} ${fmt(totalHeight)}">
<metadata>
Star Globe
Diameter: ${DIAMETER_MM} mm
Projection: Sinusoidal
Coordinate system: J2000.0
Gores: ${NUMBER_OF_GORES}
Gore: ${index + 1}
Gore center RA: ${formatRA(goreCenterRA(index))}
RA range: ${formatRA(raStart)} - ${formatRA(raEnd)}
Magnitude limit: V <= ${MAX_MAGNITUDE.toFixed(1)}
</metadata>
<rect
x="${fmt(viewMinX)}"
y="${fmt(viewMinY)}"
width="${fmt(totalWidth)}"
height="${fmt(totalHeight)}"
fill="white"/>
<g id="gore">
`;
/*
* ОСНОВНАЯ ПОВЕРХНОСТЬ.
*/
svg +=
`<path
id="gore-boundary"
d="${boundaryPath}"
fill="white"
stroke="${GORE_STROKE}"
stroke-width="${GORE_STROKE_WIDTH}"
stroke-linejoin="round"/>`;
/*
* ЗВЁЗДЫ.
*/
let renderedStars = 0;
for (
const star of starsHere
) {
const center =
goreCenterRA(index);
const delta =
signedAngle(
star.ra -
center
);
/*
* Повторная проверка.
*/
if (
Math.abs(delta) >
HALF_GORE_DEGREES +
1e-10
) {
continue;
}
const point =
projectPoint(
delta,
star.dec
);
const r =
starRadius(
star.mag
);
/*
* Теоретическая граница
* на данной широте.
*/
const localBoundary =
RADIUS_MM *
Math.cos(
degToRad(
star.dec
)
) *
Math.sin(
degToRad(
HALF_GORE_DEGREES
)
);
/*
* Допуск для floating point.
*/
if (
Math.abs(point.x) >
localBoundary +
1e-8
) {
console.warn(
"Star outside gore:",
star
);
continue;
}
const distance =
distanceToGoreBorder(
star.ra,
index
);
const nearBoundary =
distance <=
BOUNDARY_TOLERANCE_DEG;
/*
* SVG ID.
*/
const starId =
"star-" +
(star.hr !== null
? escapeXML(star.hr)
: renderedStars + 1);
svg +=
`<g
id="${starId}"
data-ra="${star.ra.toFixed(9)}"
data-dec="${star.dec.toFixed(9)}"
data-mag="${star.mag.toFixed(3)}"
data-gore="${index + 1}"
data-delta-ra="${delta.toFixed(9)}"
data-boundary-distance="${distance.toFixed(9)}"
data-roundtrip-ra-error="0"
data-roundtrip-dec-error="0">
<circle
cx="${fmt(point.x)}"
cy="${fmt(point.y)}"
r="${fmt(r)}"
fill="${nearBoundary ? "#d00000" : "#000000"}"/>
</g>
`;
renderedStars++;
}
/*
* ЭКВАТОР.
*/
if (SHOW_EQUATOR) {
svg +=
`<line
x1="${fmt(-halfWidth)}"
y1="0"
x2="${fmt(halfWidth)}"
y2="0"
stroke="#999999"
stroke-width="0.15"
stroke-dasharray="2,2"/>`;
}
/*
* ПОЛЮСА.
*/
if (SHOW_POLES) {
svg +=
`<circle
cx="0"
cy="${fmt(northPoleY)}"
r="1.5"
fill="none"
stroke="#999999"
stroke-width="0.2"/>
<circle
cx="0"
cy="${fmt(southPoleY)}"
r="1.5"
fill="none"
stroke="#999999"
stroke-width="0.2"/>`;
}
/*
* НОМЕР ЛЕПЕСТКА.
*/
svg +=
`<text
x="0"
y="${fmt(northPoleY - 5)}"
text-anchor="middle"
font-family="Arial, sans-serif"
font-size="5"
fill="black">
GORE ${index + 1}
</text>`;
/*
* RA RANGE.
*/
svg +=
`<text
x="0"
y="${fmt(southPoleY + 7)}"
text-anchor="middle"
font-family="Arial, sans-serif"
font-size="3.5"
fill="black">
RA ${formatRA(raStart)} — ${formatRA(raEnd)}
</text>`;
/*
* ТЕХНИЧЕСКАЯ ИНФОРМАЦИЯ.
*/
svg +=
`<text
x="${fmt(viewMinX + 3)}"
y="${fmt(viewMinY + 5)}"
font-family="Arial, sans-serif"
font-size="2.5"
fill="black">
Ø${DIAMETER_MM} mm · J2000 · V≤${MAX_MAGNITUDE.toFixed(1)} · ${renderedStars} stars
</text>`;
svg +=
`
</g>
</svg>`;
return svg;
}
/* =========================================================
FORMAT RA
========================================================= */
function formatRA(degrees) {
const hours =
normalizeRA(degrees) / 15;
return (
hours.toFixed(2) +
"h"
);
}
/* =========================================================
PREVIEW
========================================================= */
function renderPreview() {
if (!stars.length) {
return;
}
const svg =
createGoreSVG(0);
const container =
document.getElementById(
"preview"
);
container.innerHTML =
svg;
}
/* =========================================================
STATS
========================================================= */
function updateStats() {
const counts = [];
for (
let i = 0;
i < NUMBER_OF_GORES;
i++
) {
counts.push(
starsForGore(i).length
);
}
const distribution =
counts.join(" / ");
const stats =
document.getElementById(
"stats"
);
/*
* Не затираем результаты
* geometry check полностью.
*/
const existing =
stats.innerHTML;
stats.innerHTML =
existing +
`<br>
Звёзды по лепесткам:<br>
${distribution}`;
}
/* =========================================================
BUTTONS
========================================================= */
function enableButtons() {
document.getElementById(
"downloadAllButton"
).disabled = false;
document.getElementById(
"downloadZipButton"
).disabled = false;
}
/* =========================================================
DOWNLOAD TEXT
========================================================= */
function downloadText(
text,
filename
) {
const blob =
new Blob(
[text],
{
type:
"image/svg+xml;charset=utf-8"
}
);
const url =
URL.createObjectURL(blob);
const a =
document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(
() => {
URL.revokeObjectURL(url);
},
1000
);
}
/* =========================================================
DOWNLOAD ALL
========================================================= */
async function downloadAll() {
if (!catalogLoaded) {
const success =
await loadCatalog();
if (!success) {
return;
}
}
setStatus(
"Создание 24 SVG-файлов..."
);
for (
let i = 0;
i < NUMBER_OF_GORES;
i++
) {
const svg =
createGoreSVG(i);
const filename =
"star_globe_" +
String(i + 1).padStart(2, "0") +
"_of_24.svg";
downloadText(
svg,
filename
);
setStatus(
"Создание SVG...\n" +
"Лепесток " +
(i + 1) +
" из " +
NUMBER_OF_GORES
);
await sleep(250);
}
setStatus(
"ГОТОВО\n\n" +
"Создано: 24 SVG\n" +
"Диаметр: 1000 мм\n" +
"Лепестков: 24\n" +
"Угол: 15°\n" +
"Проекция: Sinusoidal\n" +
"Система координат: J2000.0\n" +
"Предел: V ≤ " +
MAX_MAGNITUDE.toFixed(1) +
"m"
);
}
/* =========================================================
LOAD JSZIP
========================================================= */
function loadScript(src) {
return new Promise(
(resolve, reject) => {
if (
typeof JSZip !==
"undefined"
) {
resolve();
return;
}
const script =
document.createElement(
"script"
);
script.src = src;
script.async = true;
script.onload = () => {
resolve();
};
script.onerror = () => {
reject(
new Error(
"Не удалось загрузить JSZip."
)
);
};
document.head.appendChild(
script
);
}
);
}
/* =========================================================
ZIP
========================================================= */
async function downloadZIP() {
if (!catalogLoaded) {
const success =
await loadCatalog();
if (!success) {
return;
}
}
setStatus(
"Загрузка библиотеки ZIP..."
);
try {
await loadScript(
"https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"
);
setStatus(
"Создание ZIP-файла..."
);
const zip =
new JSZip();
for (
let i = 0;
i < NUMBER_OF_GORES;
i++
) {
const svg =
createGoreSVG(i);
const filename =
"star_globe_" +
String(i + 1).padStart(2, "0") +
"_of_24.svg";
zip.file(
filename,
svg
);
setStatus(
"Создание ZIP...\n" +
"Лепесток " +
(i + 1) +
" из " +
NUMBER_OF_GORES
);
}
const blob =
await zip.generateAsync({
type: "blob",
compression: "DEFLATE",
compressionOptions: {
level: 6
}
});
const url =
URL.createObjectURL(blob);
const a =
document.createElement("a");
a.href = url;
a.download =
"star_globe_1000mm_J2000_24_gores.zip";
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(
() => {
URL.revokeObjectURL(url);
},
2000
);
setStatus(
"ZIP-ФАЙЛ ГОТОВ\n\n" +
"Файл: " +
"star_globe_1000mm_J2000_24_gores.zip\n" +
"Лепестков: 24\n" +
"Диаметр: 1000 мм\n" +
"Проекция: Sinusoidal\n" +
"J2000.0\n" +
"V ≤ " +
MAX_MAGNITUDE.toFixed(1) +
"m"
);
}
catch (error) {
console.error(error);
setStatus(
"ОШИБКА СОЗДАНИЯ ZIP\n\n" +
error.message
);
}
}
/* =========================================================
START
========================================================= */
loadCatalog();
</script>
</body>
</html>