Загрузка данных


<!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, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&apos;");
}


/* =========================================================
   ПАРСИНГ 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>