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


<!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%.
        Звёзды ярче 4.0m подписываются.
        В лепестке №1 дополнительно выделены 5 самых ярких
        звёзд северной части.
    </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;


/*
 * Порог для подписей.
 *
 * Все звёзды ярче 4.0m
 * получают подпись.
 */
const LABEL_MAGNITUDE = 4.0;


/*
 * Количество особо выделенных
 * звёзд в верхней части
 * первого лепестка.
 */
const HIGHLIGHTED_COUNT = 5;


/*
 * Технический отступ.
 *
 * Клапанов нет.
 */
const MARGIN_MM = 12;


/*
 * Обводка лепестка.
 */
const GORE_STROKE = "#999999";

const GORE_STROKE_WIDTH = 0.2;


/*
 * Каталог Yale Bright Star Catalog.
 *
 * J2000 координаты.
 */
const CATALOG_URL =
    "https://brettonw.github.io/YaleBrightStarCatalog/bsc5.json";


let stars = [];

let catalogLoaded = false;


/*
 * Особо выделенные 5 звёзд
 * первого лепестка.
 */
let highlightedStars = [];

let highlightedSet =
    new Set();



/* =========================================================
   ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
   ========================================================= */


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;

}


/*
 * Экранирование текста для SVG/XML.
 */
function escapeXML(value) {

    return String(value ?? "")
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&apos;");

}


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
    );

}



/* =========================================================
   ПАРСИНГ 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 getStarName(star) {

    /*
     * В первую очередь Common.
     */
    if (
        star.name !== null &&
        star.name !== undefined &&
        String(star.name).trim() !== ""
    ) {

        return String(star.name).trim();

    }


    /*
     * Если Common отсутствует,
     * используем HR.
     */
    if (
        star.hr !== null &&
        star.hr !== undefined &&
        String(star.hr).trim() !== ""
    ) {

        return "HR " + String(star.hr).trim();

    }


    /*
     * Последний резервный вариант.
     */
    return "Star";

}



/* =========================================================
   ЗАГРУЗКА КАТАЛОГА
   ========================================================= */


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:
                    record.Common ??
                    null

            });

        }


        stars = result;


        if (!stars.length) {

            throw new Error(
                "После обработки каталога " +
                "не осталось звёзд."
            );

        }


        catalogLoaded = true;


        /*
         * Определяем 5 самых ярких
         * звёзд верхней части
         * первого лепестка.
         */
        prepareHighlightedStars();


        setStatus(

            "Каталог загружен.\n" +

            "Всего записей в JSON: " +
            data.length +

            "\n" +

            "Звёзд с V ≤ " +
            MAX_MAGNITUDE.toFixed(1) +
            "m: " +
            stars.length +

            "\n" +

            "Подписей V < " +
            LABEL_MAGNITUDE.toFixed(1) +
            "m: " +
            countLabelStars() +

            "\n" +

            "Выделено звёзд в верхней части GORE 1: " +
            highlightedStars.length

        );


        updateStats();

        enableButtons();

        renderPreview();


        return true;

    }


    catch (error) {

        console.error(
            "Ошибка загрузки каталога:",
            error
        );


        setStatus(

            "ОШИБКА ЗАГРУЗКИ КАТАЛОГА\n\n" +

            error.message +

            "\n\n" +

            "Если HTML открыт напрямую через file://,\n" +

            "запустите его через локальный HTTP-сервер."

        );


        return false;

    }

}



/* =========================================================
   ПОДГОТОВКА 5 САМЫХ ЯРКИХ ЗВЁЗД
   В ВЕРХНЕЙ ЧАСТИ ПЕРВОГО ЛЕПЕСТКА
   ========================================================= */


function prepareHighlightedStars() {

    /*
     * GORE 1:
     *
     * центр = 0°
     *
     * диапазон:
     * -7.5° ... +7.5°
     *
     * Верхняя часть:
     * положительная declination.
     */

    const firstGoreStars =
        starsForGore(0);


    highlightedStars =
        firstGoreStars

            .filter(star => {

                return star.dec > 0;

            })

            .sort((a, b) => {

                return a.mag - b.mag;

            })

            .slice(
                0,
                HIGHLIGHTED_COUNT
            );


    highlightedSet =
        new Set(highlightedStars);

}



/* =========================================================
   КОЛИЧЕСТВО ПОДПИСЕЙ
   ========================================================= */


function countLabelStars() {

    return stars.filter(star => {

        return star.mag < LABEL_MAGNITUDE;

    }).length;

}



/* =========================================================
   СТАТИСТИКА
   ========================================================= */


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 highlightedStarRadius(mag) {

    const brightness =
        Math.max(
            0,
            Math.min(
                5,
                5 - mag
            )
        );


    const r =
        0.9 +
        Math.pow(
            brightness / 5,
            0.65
        ) * 1.8;


    return Math.max(
        0.8,
        Math.min(
            2.2,
            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 геометрия.
 *
 * x = R * cos(phi) * sin(lambda)
 *
 * y = R * phi
 *
 * phi    = широта
 *
 * lambda = отклонение от
 *          центрального меридиана
 */


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;


    /*
     * Клапанов нет.
     */

    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>`;


    /*
     * ОСНОВНАЯ ПОВЕРХНОСТЬ
     */

    svg += `<path
        d="${boundaryPath}"
        fill="white"
        stroke="${GORE_STROKE}"
        stroke-width="${GORE_STROKE_WIDTH}"
        stroke-linejoin="round"/>`;



    /* =====================================================
       СНАЧАЛА РИСУЕМ ОБЫЧНЫЕ ЗВЁЗДЫ
       ===================================================== */


    for (
        const star of starsHere
    ) {


        /*
         * Особо выделенные звёзды
         * первого лепестка пропускаем.
         *
         * Они будут нарисованы
         * отдельным блоком ниже.
         */

        if (
            index === 0 &&
            highlightedSet.has(star)
        ) {

            continue;

        }


        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"/>`;



        /* =================================================
           ПОДПИСЬ ЗВЁЗДЫ ЯРЧЕ 4.0m
           ================================================= */


        if (
            star.mag <
            LABEL_MAGNITUDE
        ) {


            const label =
                escapeXML(
                    getStarName(star)
                );


            /*
             * Подпись располагается
             * с внешней стороны звезды.
             */

            const labelOffsetX =
                point.x >= 0
                    ? -3.5
                    : 3.5;


            const anchor =
                point.x >= 0
                    ? "end"
                    : "start";


            /*
             * Линия-указатель.
             */

            svg += `<line
                x1="${fmt(point.x)}"
                y1="${fmt(point.y)}"

                x2="${fmt(
                    point.x +
                    labelOffsetX
                )}"
                y2="${fmt(
                    point.y - 2
                )}"

                stroke="#555555"
                stroke-width="0.12"/>`;


            /*
             * Название.
             */

            svg += `<text
                x="${fmt(
                    point.x +
                    labelOffsetX
                )}"

                y="${fmt(
                    point.y - 2.5
                )}"

                text-anchor="${anchor}"

                font-family="Arial, sans-serif"
                font-size="3.2"

                fill="black">

                ${label}

            </text>`;


            /*
             * Звёздная величина.
             */

            svg += `<text
                x="${fmt(
                    point.x +
                    labelOffsetX
                )}"

                y="${fmt(
                    point.y + 1.2
                )}"

                text-anchor="${anchor}"

                font-family="Arial, sans-serif"
                font-size="2.2"

                fill="#555555">

                ${star.mag.toFixed(2)}m

            </text>`;

        }

    }



    /* =====================================================
       5 САМЫХ ЯРКИХ ЗВЁЗД ПЕРВОГО ЛЕПЕСТКА
       ===================================================== */


    if (index === 0) {


        for (
            let i = 0;
            i < highlightedStars.length;
            i++
        ) {


            const star =
                highlightedStars[i];


            const delta =
                signedAngle(
                    star.ra -
                    goreCenterRA(index)
                );


            const point =
                projectPoint(
                    delta,
                    star.dec
                );


            const r =
                highlightedStarRadius(
                    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"

                stroke="white"
                stroke-width="0.35"/>`;



            /*
             * Название.
             */

            const label =
                escapeXML(
                    getStarName(star)
                );


            /*
             * Подпись смещаем
             * наружу от центра.
             */

            const labelOffsetX =
                point.x >= 0
                    ? -5
                    : 5;


            const anchor =
                point.x >= 0
                    ? "end"
                    : "start";


            /*
             * Линия-указатель.
             */

            svg += `<line
                x1="${fmt(point.x)}"
                y1="${fmt(point.y)}"

                x2="${fmt(
                    point.x +
                    labelOffsetX
                )}"

                y2="${fmt(
                    point.y - 3
                )}"

                stroke="#333333"
                stroke-width="0.15"/>`;


            /*
             * Крупное название.

             */

            svg += `<text
                x="${fmt(
                    point.x +
                    labelOffsetX
                )}"

                y="${fmt(
                    point.y - 3
                )}"

                text-anchor="${anchor}"

                font-family="Arial, sans-serif"

                font-size="4"

                font-weight="bold"

                fill="black">

                ${label}

            </text>`;


            /*
             * Величина.

             */

            svg += `<text
                x="${fmt(
                    point.x +
                    labelOffsetX
                )}"

                y="${fmt(
                    point.y + 2
                )}"

                text-anchor="${anchor}"

                font-family="Arial, sans-serif"

                font-size="2.7"

                fill="#444444">

                ${star.mag.toFixed(2)}m

            </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" +

        "Подписаны звёзды: V < " +

        LABEL_MAGNITUDE.toFixed(1) +

        "m"

    );

}



/* =========================================================
   СКАЧИВАНИЕ ТЕКСТА
   ========================================================= */


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" +

            "Подписаны звёзды: V < " +

            LABEL_MAGNITUDE.toFixed(1) +

            "m"

        );


    }

    catch (error) {

        console.error(error);


        setStatus(

            "ОШИБКА СОЗДАНИЯ ZIP\n\n" +

            error.message

        );

    }

}



/* =========================================================
   ЗАПУСК
   ========================================================= */


loadCatalog();

</script>

</body>
</html>