Загрузка данных
<!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: 1000px;
margin: 0 auto 20px;
padding: 20px;
background: white;
border-radius: 8px;
box-sizing: border-box;
}
h1, h2 {
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;
}
</style>
</head>
<body>
<div class="panel">
<h1>Звёздный глобус Ø1000 мм</h1>
<p>
<b>24 лепестка</b> ·
<b>J2000</b> ·
звёзды до <b>5.0m</b>
</p>
<p class="small">
Чёрные звёзды на белом фоне.
Каждый лепесток охватывает 15° прямого восхождения.
SVG имеют физические размеры в миллиметрах.
Обводка лепестка — серая, 0,2 мм.
Под каждой звездой/рядом со звездой выводится её обозначение мелким шрифтом.
При печати необходимо использовать масштаб 100%.
</p>
<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>
<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 MAX_MAGNITUDE = 5.0;
/*
* MARGIN используется только как внешний
* технический отступ вокруг геометрии.
*/
const MARGIN_MM = 12;
/*
* Обводка лепестка.
*/
const GORE_STROKE = "#999999";
const GORE_STROKE_WIDTH = 0.2;
/*
* Размер шрифта подписей звёзд.
*
* 2 мм — мелкий, но пригодный
* для печати размер.
*/
const STAR_LABEL_FONT_SIZE = 2.0;
/*
* Цвет подписей.
*/
const STAR_LABEL_COLOR = "#111111";
/*
* Отступ подписи от звезды.
*/
const STAR_LABEL_OFFSET_X = 1.5;
const STAR_LABEL_OFFSET_Y = -1.0;
/*
* Yale Bright Star Catalog.
*
* J2000 координаты.
*/
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) {
const element =
document.getElementById("status");
element.textContent = text;
}
function normalizeRA(ra) {
let value =
Number(ra) % 360;
if (value < 0) {
value += 360;
}
return value;
}
/*
* Угол в диапазоне [-180, +180).
*/
function signedAngle(deg) {
return (
((deg + 180) % 360 + 360) % 360 - 180
);
}
/*
* Экранирование текста для SVG/XML.
*
* Необходимо, чтобы &, <, >, ", '
* внутри названий звёзд не ломали SVG.
*/
function escapeXML(value) {
return String(value)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
/* =========================================================
ПАРСИНГ RA
========================================================= */
function parseRA(value) {
if (
value === undefined ||
value === null
) {
return null;
}
if (typeof value === "number") {
if (!Number.isFinite(value)) {
return null;
}
/*
* Числовое RA из каталога
* трактуем как часы,
* если оно находится в диапазоне 0...24.
*/
if (
value >= 0 &&
value <= 24
) {
return 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, " ");
const parts =
text
.split(" ")
.filter(Boolean);
if (!parts.length) {
return null;
}
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;
}
return normalizeRA(
15 * (
h +
m / 60 +
s / 3600
)
);
}
/* =========================================================
ПАРСИНГ DECLINATION
========================================================= */
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 text =
String(value)
.trim()
.replace(/°/g, " ")
.replace(/′/g, " ")
.replace(/″/g, " ")
.replace(/:/g, " ")
.replace(/,/g, ".")
.replace(/\s+/g, " ");
if (!text) {
return null;
}
const negative =
text.startsWith("-") ||
text.startsWith("−");
const cleaned =
text.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;
}
let dec =
Math.abs(d) +
Math.abs(m) / 60 +
Math.abs(s) / 3600;
if (negative) {
dec = -dec;
}
if (
dec < -90 ||
dec > 90
) {
return null;
}
return dec;
}
/* =========================================================
ПОЛУЧЕНИЕ 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;
}
/* =========================================================
ПОЛУЧЕНИЕ DECLINATION
========================================================= */
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;
}
/* =========================================================
ПОЛУЧЕНИЕ НАЗВАНИЯ ЗВЕЗДЫ
========================================================= */
function getStarLabel(record) {
/*
* Основное имя.
*/
if (
record.Common !== undefined &&
record.Common !== null &&
String(record.Common).trim() !== ""
) {
return String(record.Common).trim();
}
/*
* Если Common отсутствует,
* используем HR.
*/
if (
record.HR !== undefined &&
record.HR !== null &&
String(record.HR).trim() !== ""
) {
return "HR " + String(record.HR).trim();
}
/*
* Дополнительные возможные поля
* каталога — на случай изменения
* структуры JSON.
*/
const possibleFields = [
"Name",
"name",
"Bayer",
"Flamsteed",
"HD"
];
for (
const field of possibleFields
) {
if (
record[field] !== undefined &&
record[field] !== null &&
String(record[field]).trim() !== ""
) {
return String(record[field]).trim();
}
}
return "";
}
/* =========================================================
ЗАГРУЗКА КАТАЛОГА
========================================================= */
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;
}
if (
ra < 0 ||
ra >= 360 ||
dec < -90 ||
dec > 90
) {
continue;
}
result.push({
ra: ra,
dec: dec,
mag: mag,
hr:
record.HR ??
null,
name:
getStarLabel(record)
});
}
stars = result;
if (!stars.length) {
throw new Error(
"После обработки каталога " +
"не осталось звёзд."
);
}
catalogLoaded = true;
const labeledCount =
stars.filter(
star =>
star.name &&
star.name.trim() !== ""
).length;
setStatus(
"Каталог загружен.\n" +
"Всего записей в JSON: " +
data.length +
"\n" +
"Звёзд с V ≤ " +
MAX_MAGNITUDE.toFixed(1) +
"m: " +
stars.length +
"\n" +
"Звёзд с подписями: " +
labeledCount
);
updateStats();
enableButtons();
renderPreview();
return true;
}
catch (error) {
console.error(
"Ошибка загрузки каталога:",
error
);
setStatus(
"ОШИБКА ЗАГРУЗКИ КАТАЛОГА\n\n" +
error.message +
"\n\n" +
"Если HTML открыт напрямую через file://,\n" +
"запустите его через локальный HTTP-сервер."
);
return false;
}
}
/* =========================================================
СТАТИСТИКА
========================================================= */
function updateStats() {
const element =
document.getElementById("stats");
const counts = [];
for (
let i = 0;
i < NUMBER_OF_GORES;
i++
) {
counts.push(
starsForGore(i).length
);
}
element.textContent =
"Распределение звёзд по лепесткам: " +
counts.join(" / ");
}
/* =========================================================
РАЗМЕР ЗВЕЗДЫ
========================================================= */
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
)
);
}
/* =========================================================
ЦЕНТР ЛЕПЕСТКА
========================================================= */
function goreCenterRA(index) {
return (
index *
GORE_DEGREES
);
}
/* =========================================================
ЗВЁЗДЫ ЛЕПЕСТКА
========================================================= */
function starsForGore(index) {
const center =
goreCenterRA(index);
const half =
GORE_DEGREES / 2;
const EPS =
1e-8;
return stars.filter(star => {
const delta =
signedAngle(
star.ra - center
);
return (
delta >= -half - EPS &&
delta < half - EPS
);
});
}
/* =========================================================
ГЕОМЕТРИЯ ЛЕПЕСТКА
========================================================= */
/*
* Для каждого лепестка используется
* сферическая sinusoidal-gore геометрия.
*
* Координаты:
*
* y = R * phi
*
* x = R * cos(phi) * sin(lambda)
*
* где:
*
* phi = широта
* lambda = отклонение от центрального меридиана.
*
* Границы находятся на:
*
* lambda = ±7.5°
*
* Поэтому лепесток:
*
* - максимален на экваторе;
* - сужается к полюсам;
* - заканчивается точно в полюсах.
*/
function projectPoint(
deltaRA,
dec
) {
const lambda =
degToRad(deltaRA);
const phi =
degToRad(dec);
const x =
RADIUS_MM *
Math.cos(phi) *
Math.sin(lambda);
const y =
RADIUS_MM *
phi;
return {
x: x,
y: y
};
}
/* =========================================================
ГРАНИЦА ЛЕПЕСТКА
========================================================= */
function goreBoundary() {
const half =
degToRad(
GORE_DEGREES / 2
);
const points = [];
/*
* Левая граница:
* северный полюс → южный полюс.
*/
for (
let i = 0;
i <= 720;
i++
) {
const phi =
Math.PI / 2 -
Math.PI * i / 720;
const x =
RADIUS_MM *
Math.cos(phi) *
Math.sin(-half);
const y =
RADIUS_MM *
phi;
points.push({
x: x,
y: y
});
}
/*
* Правая граница:
* южный полюс → северный полюс.
*/
for (
let i = 720;
i >= 0;
i--
) {
const phi =
Math.PI / 2 -
Math.PI * i / 720;
const x =
RADIUS_MM *
Math.cos(phi) *
Math.sin(half);
const y =
RADIUS_MM *
phi;
points.push({
x: x,
y: y
});
}
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);
}
d += " Z";
return d;
}
/* =========================================================
SVG ОДНОГО ЛЕПЕСТКА
========================================================= */
function createGoreSVG(index) {
const starsHere =
starsForGore(index);
const halfWidth =
RADIUS_MM *
Math.sin(
degToRad(
GORE_DEGREES / 2
)
);
const goreWidth =
halfWidth * 2;
/*
* Клапанов больше нет.
*
* Поэтому ширина SVG =
* ширина лепестка + только
* технические поля.
*/
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);
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)}">
<rect
x="${fmt(viewMinX)}"
y="${fmt(viewMinY)}"
width="${fmt(totalWidth)}"
height="${fmt(totalHeight)}"
fill="white"/>
<g>
`;
/*
* ОСНОВНАЯ ПОВЕРХНОСТЬ
*
* Белая заливка.
*
* Серая обводка:
* 0.2 мм.
*/
svg +=
`<path
d="${boundaryPath}"
fill="white"
stroke="${GORE_STROKE}"
stroke-width="${GORE_STROKE_WIDTH}"
stroke-linejoin="round"/>`;
/*
* =====================================================
* ЗВЁЗДЫ И ПОДПИСИ
* =====================================================
*/
for (
const star of starsHere
) {
const delta =
signedAngle(
star.ra -
goreCenterRA(index)
);
const point =
projectPoint(
delta,
star.dec
);
const r =
starRadius(
star.mag
);
/*
* Защита от выхода
* за границу лепестка.
*/
if (
Math.abs(point.x) >
halfWidth + 0.001
) {
continue;
}
/*
* -----------------------------------------------
* САМА ЗВЕЗДА
* -----------------------------------------------
*/
svg +=
`<circle
cx="${fmt(point.x)}"
cy="${fmt(point.y)}"
r="${fmt(r)}"
fill="black"/>`;
/*
* -----------------------------------------------
* ПОДПИСЬ ЗВЕЗДЫ
* -----------------------------------------------
*/
const label =
star.name
? String(star.name).trim()
: (
star.hr !== null &&
star.hr !== undefined
? "HR " + String(star.hr)
: ""
);
/*
* Если у звезды вообще
* нет обозначения — пропускаем
* только текст, сама звезда
* всё равно остаётся.
*/
if (label) {
/*
* Слева от центра лепестка
* текст выравнивается вправо.
*
* Справа — влево.
*
* Это уменьшает вероятность
* наложения текста на звезду.
*/
const direction =
point.x >= 0
? 1
: -1;
const labelAnchor =
direction > 0
? "start"
: "end";
const labelX =
point.x +
direction *
STAR_LABEL_OFFSET_X;
const labelY =
point.y +
STAR_LABEL_OFFSET_Y;
svg +=
`<text
x="${fmt(labelX)}"
y="${fmt(labelY)}"
text-anchor="${labelAnchor}"
dominant-baseline="middle"
font-family="Arial, sans-serif"
font-size="${fmt(STAR_LABEL_FONT_SIZE)}"
fill="${STAR_LABEL_COLOR}"
stroke="none">${escapeXML(label)}</text>`;
}
}
/*
* =====================================================
* ЭКВАТОР
* =====================================================
*
* Оставляем тонкую пунктирную
* техническую линию.
*/
svg +=
`<line
x1="${fmt(-halfWidth)}"
y1="0"
x2="${fmt(halfWidth)}"
y2="0"
stroke="#999999"
stroke-width="0.15"
stroke-dasharray="2,2"/>`;
/*
* =====================================================
* ПОЛЮСА
* =====================================================
*/
const poleY =
Math.PI *
RADIUS_MM /
2;
svg +=
`<circle
cx="0"
cy="${fmt(-poleY)}"
r="1.5"
fill="none"
stroke="#999999"
stroke-width="0.2"/>
<circle
cx="0"
cy="${fmt(poleY)}"
r="1.5"
fill="none"
stroke="#999999"
stroke-width="0.2"/>`;
/*
* =====================================================
* НОМЕР ЛЕПЕСТКА
* =====================================================
*/
svg +=
`<text
x="0"
y="${fmt(-poleY - 5)}"
text-anchor="middle"
font-family="Arial, sans-serif"
font-size="5"
fill="black">
GORE ${index + 1}
</text>`;
/*
* =====================================================
* RA ДИАПАЗОН
* =====================================================
*/
const raStart =
normalizeRA(
goreCenterRA(index) -
GORE_DEGREES / 2
);
const raEnd =
normalizeRA(
goreCenterRA(index) +
GORE_DEGREES / 2
);
svg +=
`<text
x="0"
y="${fmt(poleY + 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">
Ø1000 mm · J2000 · V≤${MAX_MAGNITUDE.toFixed(1)} · ${starsHere.length} 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;
}
/* =========================================================
АКТИВАЦИЯ КНОПОК
========================================================= */
function enableButtons() {
document.getElementById(
"downloadAllButton"
).disabled = false;
document.getElementById(
"downloadZipButton"
).disabled = false;
}
/* =========================================================
СКАЧАТЬ 24 SVG
========================================================= */
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" +
"Система координат: J2000\n" +
"Предел звёзд: V ≤ " +
MAX_MAGNITUDE.toFixed(1) +
"m\n" +
"Все доступные звёзды подписаны."
);
}
/* =========================================================
СКАЧИВАНИЕ ТЕКСТА
========================================================= */
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
);
}
/* =========================================================
ЗАГРУЗКА 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" +
"J2000\n" +
"V ≤ " +
MAX_MAGNITUDE.toFixed(1) +
"m\n" +
"Все доступные звёзды подписаны."
);
}
catch (error) {
console.error(error);
setStatus(
"ОШИБКА СОЗДАНИЯ ZIP\n\n" +
error.message
);
}
}
/* =========================================================
ЗАПУСК
========================================================= */
loadCatalog();
</script>
</body>
</html>