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


<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">

<title>Celestial Globe 1500mm — 24 Orange Peel Gores</title>

<style>

body {
    font-family: Arial, sans-serif;
    background: #10131a;
    color: white;
    margin: 40px;
}

button {
    background: #2878ff;
    color: white;
    border: 0;
    padding: 15px 25px;
    font-size: 18px;
    border-radius: 8px;
    cursor: pointer;
    margin-right: 10px;
    margin-top: 10px;
}

button:hover {
    background: #4b91ff;
}

button:disabled {
    background: #555;
    cursor: not-allowed;
}

#fileInput {
    display: none;
}

#fileName {
    margin-top: 15px;
    color: #9fc5ff;
}

#status {
    margin-top: 25px;
    white-space: pre-line;
    font-family: monospace;
    color: #bcd7ff;
    line-height: 1.5;
}

h1 {
    font-size: 28px;
}

.info {
    max-width: 900px;
    line-height: 1.5;
}

.warning {
    color: #ffd15a;
}

</style>
</head>

<body>

<h1>CELESTIAL GLOBE Ø1500 mm</h1>

<div class="info">

<p>
    24 классических <b>orange-peel</b> лепестка.
    HYG 4.1.
    Только звёзды с <b>mag < 5.0</b>.
</p>

<p>
    Этот вариант работает полностью локально.
    GitHub и внешний сервер не используются.
</p>

<p class="warning">
    Сначала выберите файл <b>hyg_v41.csv</b>,
    затем нажмите «СОЗДАТЬ SVG».
</p>

</div>

<input
    id="fileInput"
    type="file"
    accept=".csv,text/csv"
>

<button onclick="selectHYG()">
    ???? ВЫБРАТЬ HYG 4.1
</button>

<button
    id="generateButton"
    onclick="generate()"
    disabled
>
    ???? СОЗДАТЬ SVG
</button>

<div id="fileName">
    HYG-файл не выбран.
</div>

<div id="status">
    Готов.
</div>


<script>

/* ============================================================
   SETTINGS
   ============================================================ */

const DIAMETER_MM = 1500.0;
const RADIUS_MM = DIAMETER_MM / 2.0;

const NUM_GORES = 24;

const GORE_WIDTH_DEG =
    360.0 / NUM_GORES;

const HALF_GORE_DEG =
    GORE_WIDTH_DEG / 2.0;

const MAG_LIMIT = 5.0;


/* ============================================================
   STAR SIZE
   ============================================================ */

const STAR_MIN_MM = 0.8;
const STAR_MAX_MM = 4.5;


/* ============================================================
   RESOLUTION
   ============================================================ */

const LAT_STEPS = 360;


/* ============================================================
   LAYOUT
   ============================================================ */

const COLUMNS = 6;
const ROWS = 4;

const GORE_GAP_X = 100.0;
const GORE_GAP_Y = 180.0;

const PAGE_MARGIN = 150.0;


/* ============================================================
   DIMENSIONS
   ============================================================ */

const GORE_EQUATOR_WIDTH =
    2.0 *
    RADIUS_MM *
    Math.sin(
        HALF_GORE_DEG *
        Math.PI /
        180.0
    );

const GORE_HEIGHT =
    Math.PI *
    RADIUS_MM;

const CELL_WIDTH =
    GORE_EQUATOR_WIDTH +
    GORE_GAP_X;

const CELL_HEIGHT =
    GORE_HEIGHT +
    GORE_GAP_Y;

const SVG_WIDTH =
    2.0 *
    PAGE_MARGIN +
    COLUMNS *
    CELL_WIDTH;

const SVG_HEIGHT =
    2.0 *
    PAGE_MARGIN +
    ROWS *
    CELL_HEIGHT +
    200.0;


/* ============================================================
   GLOBAL DATA
   ============================================================ */

let hygText = null;


/* ============================================================
   STATUS
   ============================================================ */

function status(text) {

    document.getElementById(
        "status"
    ).textContent = text;

}


/* ============================================================
   SELECT HYG
   ============================================================ */

function selectHYG() {

    const input =
        document.getElementById(
            "fileInput"
        );

    input.click();

}


/* ============================================================
   FILE INPUT
   ============================================================ */

document
    .getElementById("fileInput")
    .addEventListener(
        "change",
        async function(event) {

            const file =
                event.target.files[0];

            if (!file) {
                return;
            }

            try {

                status(
                    "Читаю HYG 4.1...\n" +
                    file.name
                );

                hygText =
                    await file.text();

                if (
                    !hygText ||
                    hygText.length < 1000
                ) {

                    throw new Error(
                        "Файл пустой или повреждён."
                    );

                }

                document.getElementById(
                    "fileName"
                ).textContent =
                    "Выбран файл: " +
                    file.name +
                    " (" +
                    Math.round(
                        file.size / 1024 / 1024
                    ) +
                    " MB)";

                document.getElementById(
                    "generateButton"
                ).disabled = false;

                status(
                    "HYG загружен в память.\n\n" +
                    "Теперь нажмите «СОЗДАТЬ SVG»."
                );

            }
            catch(error) {

                hygText = null;

                document.getElementById(
                    "generateButton"
                ).disabled = true;

                status(
                    "ОШИБКА ЗАГРУЗКИ HYG:\n\n" +
                    error.message
                );

            }

        }
    );


/* ============================================================
   MATH
   ============================================================ */

function radians(deg) {

    return deg *
        Math.PI /
        180.0;

}


function normalize360(deg) {

    return (
        (
            deg % 360.0
        ) +
        360.0
    ) % 360.0;

}


function normalize180(deg) {

    return (
        (
            (
                deg + 180.0
            ) % 360.0 +
            360.0
        ) % 360.0
    ) - 180.0;

}


/* ============================================================
   RA
   ============================================================ */

function raHoursToDegrees(
    raHours
) {

    return normalize360(
        raHours * 15.0
    );

}


/* ============================================================
   GORE
   ============================================================ */

function goreCenter(
    goreIndex
) {

    return goreIndex *
        GORE_WIDTH_DEG;

}


function getGore(
    raDeg
) {

    const ra =
        normalize360(
            raDeg
        );

    return Math.floor(
        (
            ra +
            HALF_GORE_DEG
        ) /
        GORE_WIDTH_DEG
    ) % NUM_GORES;

}


function localLongitude(
    raDeg,
    goreIndex
) {

    return normalize180(
        raDeg -
        goreCenter(
            goreIndex
        )
    );

}


/* ============================================================
   ORANGE PEEL PROJECTION
   ============================================================ */

function orangePeel(
    latitudeDeg,
    longitudeDeg
) {

    const phi =
        radians(
            latitudeDeg
        );

    const lambda =
        radians(
            longitudeDeg
        );

    const x =
        RADIUS_MM *
        lambda *
        Math.cos(phi);

    const y =
        RADIUS_MM *
        phi;

    return {
        x: x,
        y: y
    };

}


/* ============================================================
   LAYOUT
   ============================================================ */

function layoutOffset(
    goreIndex
) {

    const col =
        goreIndex %
        COLUMNS;

    const row =
        Math.floor(
            goreIndex /
            COLUMNS
        );

    return {

        x:
            PAGE_MARGIN +
            col *
            CELL_WIDTH +
            GORE_EQUATOR_WIDTH /
            2.0,

        y:
            PAGE_MARGIN +
            row *
            CELL_HEIGHT +
            GORE_HEIGHT /
            2.0

    };

}


/* ============================================================
   SVG ESCAPE
   ============================================================ */

function esc(text) {

    return String(text)
        .replace(
            /&/g,
            "&"
        )
        .replace(
            /</g,
            "<"
        )
        .replace(
            />/g,
            ">"
        )
        .replace(
            /"/g,
            """
        );

}


/* ============================================================
   PATH
   ============================================================ */

function pointsToPath(
    points,
    close
) {

    if (!points.length) {
        return "";
    }

    let d =
        "M " +
        points[0].x.toFixed(4) +
        " " +
        points[0].y.toFixed(4);

    for (
        let i = 1;
        i < points.length;
        i++
    ) {

        d +=
            " L " +
            points[i].x.toFixed(4) +
            " " +
            points[i].y.toFixed(4);

    }

    if (close) {
        d += " Z";
    }

    return d;

}


/* ============================================================
   GORE OUTLINE
   ============================================================ */

function createGorePath(
    goreIndex
) {

    const offset =
        layoutOffset(
            goreIndex
        );

    let points = [];


    /* LEFT EDGE */

    for (
        let i = 0;
        i <= LAT_STEPS;
        i++
    ) {

        const lat =
            -90.0 +
            180.0 *
            i /
            LAT_STEPS;

        const p =
            orangePeel(
                lat,
                -HALF_GORE_DEG
            );

        points.push({

            x:
                offset.x +
                p.x,

            y:
                offset.y -
                p.y

        });

    }


    /* RIGHT EDGE */

    let right = [];

    for (
        let i = 0;
        i <= LAT_STEPS;
        i++
    ) {

        const lat =
            -90.0 +
            180.0 *
            i /
            LAT_STEPS;

        const p =
            orangePeel(
                lat,
                HALF_GORE_DEG
            );

        right.push({

            x:
                offset.x +
                p.x,

            y:
                offset.y -
                p.y

        });

    }


    right.reverse();

    points =
        points.concat(
            right
        );


    return pointsToPath(
        points,
        true
    );

}


/* ============================================================
   STAR SIZE
   ============================================================ */

function starDiameter(
    mag
) {

    let t =
        (
            5.0 -
            mag
        ) / 6.5;

    t =
        Math.max(
            0.0,
            Math.min(
                1.0,
                t
            )
        );

    t =
        Math.pow(
            t,
            0.55
        );

    return (
        STAR_MIN_MM +
        t *
        (
            STAR_MAX_MM -
            STAR_MIN_MM
        )
    );

}


/* ============================================================
   CSV PARSER
   ============================================================ */

function parseCSV(
    text
) {

    const rows = [];

    let row = [];
    let field = "";

    let insideQuotes =
        false;


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

        const c =
            text[i];


        if (c === '"') {

            if (
                insideQuotes &&
                text[i + 1] === '"'
            ) {

                field += '"';
                i++;

            }
            else {

                insideQuotes =
                    !insideQuotes;

            }

            continue;

        }


        if (
            c === "," &&
            !insideQuotes
        ) {

            row.push(field);

            field = "";

            continue;

        }


        if (
            (
                c === "\n" ||
                c === "\r"
            ) &&
            !insideQuotes
        ) {

            if (
                c === "\r" &&
                text[i + 1] === "\n"
            ) {

                i++;

            }

            row.push(field);

            field = "";


            if (row.length) {

                rows.push(row);

            }

            row = [];

            continue;

        }


        field += c;

    }


    if (
        field.length ||
        row.length
    ) {

        row.push(field);

        if (row.length) {

            rows.push(row);

        }

    }


    return rows;

}


/* ============================================================
   GENERATE
   ============================================================ */

async function generate() {

    if (!hygText) {

        status(
            "Сначала выберите hyg_v41.csv."
        );

        return;

    }


    try {

        /* ====================================================
           CSV
           ==================================================== */

        status(
            "Разбираю HYG 4.1..."
        );

        const rows =
            parseCSV(
                hygText
            );


        if (!rows.length) {

            throw new Error(
                "CSV пустой."
            );

        }


        const header =
            rows[0];


        const index = {};


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

            index[
                header[i].trim()
            ] = i;

        }


        /* ====================================================
           CHECK COLUMNS
           ==================================================== */

        if (
            index.ra === undefined ||
            index.dec === undefined ||
            index.mag === undefined
        ) {

            throw new Error(
                "Не найдены колонки ra, dec или mag.\n\n" +
                "Убедитесь, что выбран именно HYG 4.1."
            );

        }


        /* ====================================================
           READ STARS
           ==================================================== */

        const stars = [];


        for (
            let r = 1;
            r < rows.length;
            r++
        ) {

            const row =
                rows[r];


            const ra =
                parseFloat(
                    row[index.ra]
                );


            const dec =
                parseFloat(
                    row[index.dec]
                );


            const mag =
                parseFloat(
                    row[index.mag]
                );


            if (
                !Number.isFinite(ra) ||
                !Number.isFinite(dec) ||
                !Number.isFinite(mag)
            ) {

                continue;

            }


            /* STRICT mag < 5.0 */

            if (
                !(mag < MAG_LIMIT)
            ) {

                continue;

            }


            if (
                dec < -90.0 ||
                dec > 90.0
            ) {

                continue;

            }


            stars.push({

                ra: ra,

                dec: dec,

                mag: mag,

                proper:
                    index.proper !== undefined
                    ? row[index.proper]
                    : "",

                con:
                    index.con !== undefined
                    ? row[index.con]
                    : ""

            });

        }


        status(
            "Звёзд mag < 5.0: " +
            stars.length +
            "\n\n" +
            "Строю 24 лепестка..."
        );


        /* ====================================================
           SVG
           ==================================================== */

        let svg = [];


        svg.push(
`<?xml version="1.0" encoding="UTF-8"?>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="${SVG_WIDTH.toFixed(3)}mm"
height="${SVG_HEIGHT.toFixed(3)}mm"
viewBox="0 0 ${SVG_WIDTH.toFixed(3)} ${SVG_HEIGHT.toFixed(3)}">

<defs>

<style>

.gore {
    fill:#07101f;
    stroke:#ff3218;
    stroke-width:1.2;
}

.grid {
    fill:none;
    stroke:#285481;
    stroke-width:0.35;
}

.equator {
    fill:none;
    stroke:#4c91d0;
    stroke-width:0.7;
}

.star {
    fill:#ffd15a;
    stroke:none;
}

.label {
    fill:#dcecff;
    font-family:Arial,sans-serif;
}

.info {
    fill:#ffffff;
    font-family:Arial,sans-serif;
}

.cut {
    fill:none;
    stroke:#ff3218;
    stroke-width:1.2;
}

</style>

</defs>`
        );


        /* ====================================================
           BACKGROUND
           ==================================================== */

        svg.push(
            `<rect
                x="0"
                y="0"
                width="${SVG_WIDTH}"
                height="${SVG_HEIGHT}"
                fill="#03060d"/>`
        );


        /* ====================================================
           GORES
           ==================================================== */

        svg.push(
`<g
inkscape:groupmode="layer"
inkscape:label="01 GORES">`
        );


        for (
            let g = 0;
            g < NUM_GORES;
            g++
        ) {

            const path =
                createGorePath(
                    g
                );


            const center =
                goreCenter(
                    g
                );


            svg.push(
`<path
id="GORE_${String(g + 1).padStart(2,"0")}"
class="gore"
d="${path}">

<title>
GORE ${String(g + 1).padStart(2,"0")}
CENTER RA ${center.toFixed(3)} degrees
</title>

</path>`
            );

        }


        svg.push(
            "</g>"
        );


        /* ====================================================
           GRID
           ==================================================== */

        svg.push(
`<g
inkscape:groupmode="layer"
inkscape:label="02 COORDINATE GRID"
class="grid">`
        );


        for (
            let g = 0;
            g < NUM_GORES;
            g++
        ) {

            const offset =
                layoutOffset(
                    g
                );


            /* LATITUDE */

            for (
                const lat of
                [-60, -30, 30, 60]
            ) {

                let points = [];


                for (
                    let i = 0;
                    i <= 72;
                    i++
                ) {

                    const lon =
                        -HALF_GORE_DEG +
                        GORE_WIDTH_DEG *
                        i /
                        72;


                    const p =
                        orangePeel(
                            lat,
                            lon
                        );


                    points.push({

                        x:
                            offset.x +
                            p.x,

                        y:
                            offset.y -
                            p.y

                    });

                }


                svg.push(
                    `<path
                    d="${pointsToPath(
                        points,
                        false
                    )}"/>`
                );

            }


            /* CENTRAL MERIDIAN */

            let meridian = [];


            for (
                let i = 0;
                i <= LAT_STEPS;
                i++
            ) {

                const lat =
                    -90 +
                    180 *
                    i /
                    LAT_STEPS;


                const p =
                    orangePeel(
                        lat,
                        0
                    );


                meridian.push({

                    x:
                        offset.x +
                        p.x,

                    y:
                        offset.y -
                        p.y

                });

            }


            svg.push(
                `<path
                d="${pointsToPath(
                    meridian,
                    false
                )}"/>`
            );

        }


        svg.push(
            "</g>"
        );


        /* ====================================================
           EQUATOR
           ==================================================== */

        svg.push(
`<g
inkscape:groupmode="layer"
inkscape:label="03 EQUATOR"
class="equator">`
        );


        for (
            let g = 0;
            g < NUM_GORES;
            g++
        ) {

            const offset =
                layoutOffset(
                    g
                );


            let points = [];


            for (
                let i = 0;
                i <= 72;
                i++
            ) {

                const lon =
                    -HALF_GORE_DEG +
                    GORE_WIDTH_DEG *
                    i /
                    72;


                const p =
                    orangePeel(
                        0,
                        lon
                    );


                points.push({

                    x:
                        offset.x +
                        p.x,

                    y:
                        offset.y -
                        p.y

                });

            }


            svg.push(
                `<path
                d="${pointsToPath(
                    points,
                    false
                )}"/>`
            );

        }


        svg.push(
            "</g>"
        );


        /* ====================================================
           STARS
           ==================================================== */

        status(
            "Строю звёзды...\n" +
            "0 / " +
            stars.length
        );


        svg.push(
`<g
inkscape:groupmode="layer"
inkscape:label="04 STARS mag less than 5.0"
class="star">`
        );


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

            const star =
                stars[i];


            const raDeg =
                raHoursToDegrees(
                    star.ra
                );


            const decDeg =
                star.dec;


            const gore =
                getGore(
                    raDeg
                );


            const localLon =
                localLongitude(
                    raDeg,
                    gore
                );


            const p =
                orangePeel(
                    decDeg,
                    localLon
                );


            const offset =
                layoutOffset(
                    gore
                );


            const x =
                offset.x +
                p.x;


            const y =
                offset.y -
                p.y;


            const diameter =
                starDiameter(
                    star.mag
                );


            const radius =
                diameter / 2.0;


            const title =
                "RA=" +
                raDeg.toFixed(6) +
                " deg | " +
                "Dec=" +
                decDeg.toFixed(6) +
                " deg | " +
                "mag=" +
                star.mag.toFixed(2);


            svg.push(
`<circle
id="STAR_${String(i).padStart(5,"0")}"
cx="${x.toFixed(4)}"
cy="${y.toFixed(4)}"
r="${radius.toFixed(4)}"
class="star">

<title>
${esc(title)}
</title>

</circle>`
            );


            if (
                i % 500 === 0
            ) {

                status(
                    "Строю звёзды...\n" +
                    i +
                    " / " +
                    stars.length
                );

            }

        }


        svg.push(
            "</g>"
        );


        /* ====================================================
           LABELS
           ==================================================== */

        svg.push(
`<g
inkscape:groupmode="layer"
inkscape:label="05 LABELS"
class="label">`
        );


        for (
            let g = 0;
            g < NUM_GORES;
            g++
        ) {

            const offset =
                layoutOffset(
                    g
                );


            const center =
                goreCenter(
                    g
                );


            svg.push(
`<text
x="${offset.x.toFixed(3)}"
y="${(
    offset.y +
    GORE_HEIGHT / 2 +
    80
).toFixed(3)}"
text-anchor="middle"
font-size="18mm">

GORE ${String(g + 1).padStart(2,"0")}
/
RA ${center.toFixed(1)}°

</text>`
            );

        }


        svg.push(
            "</g>"
        );


        /* ====================================================
           INFO
           ==================================================== */

        svg.push(
`<g
inkscape:groupmode="layer"
inkscape:label="06 PRINT INFORMATION"
class="info">`
        );


        svg.push(
`<text
x="${(
    SVG_WIDTH / 2
).toFixed(3)}"
y="60"
text-anchor="middle"
font-size="25mm"
font-weight="bold">

CELESTIAL GLOBE Ø1500 mm

</text>`
        );


        svg.push(
`<text
x="${(
    SVG_WIDTH / 2
).toFixed(3)}"
y="95"
text-anchor="middle"
font-size="11mm">

24 ORANGE-PEEL GORES /
HYG 4.1 /
MAG < 5.0 /
J2000.0

</text>`
        );


        /* ====================================================
           SCALE BAR
           ==================================================== */

        const sx =
            PAGE_MARGIN;

        const sy =
            SVG_HEIGHT -
            110;


        svg.push(
`<line
x1="${sx}"
y1="${sy}"
x2="${sx + 100}"
y2="${sy}"
stroke="white"
stroke-width="2"/>`
        );


        svg.push(
`<line
x1="${sx}"
y1="${sy - 10}"
x2="${sx}"
y2="${sy + 10}"
stroke="white"
stroke-width="2"/>`
        );


        svg.push(
`<line
x1="${sx + 100}"
y1="${sy - 10}"
x2="${sx + 100}"
y2="${sy + 10}"
stroke="white"
stroke-width="2"/>`
        );


        svg.push(
`<text
x="${sx + 50}"
y="${sy - 20}"
text-anchor="middle"
font-size="8mm">

100 mm — PRINT 100%

</text>`
        );


        /* ====================================================
           INFO BLOCK
           ==================================================== */

        const infoX =
            SVG_WIDTH -
            PAGE_MARGIN -
            500;


        const infoY =
            SVG_HEIGHT -
            260;


        const info = [

            "DIAMETER: 1500 mm",

            "RADIUS: 750 mm",

            "GORES: 24",

            "GORE WIDTH: 15 deg",

            "STARS: mag < 5.0",

            "STAR COUNT: " +
            stars.length,

            "CATALOG: HYG 4.1",

            "EPOCH: J2000.0",

            "PROJECTION: ORANGE-PEEL",

            "SCALE: 1:1"

        ];


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

            svg.push(
`<text
x="${infoX}"
y="${infoY + i * 30}"
font-size="7mm">

${esc(info[i])}

</text>`
            );

        }


        svg.push(
            "</g>"
        );


        /* ====================================================
           CLOSE SVG
           ==================================================== */

        svg.push(
            "</svg>"
        );


        /* ====================================================
           DOWNLOAD
           ==================================================== */

        status(
            "SVG готов.\n" +
            "Создаю файл для скачивания..."
        );


        const blob =
            new Blob(
                [
                    svg.join("\n")
                ],
                {
                    type:
                    "image/svg+xml;charset=utf-8"
                }
            );


        const url =
            URL.createObjectURL(
                blob
            );


        const link =
            document.createElement(
                "a"
            );


        link.href =
            url;


        link.download =
            "celestial_globe_1500mm_24_orange_peel_mag_lt_5.svg";


        document.body.appendChild(
            link
        );


        link.click();


        document.body.removeChild(
            link
        );


        setTimeout(
            function() {
                URL.revokeObjectURL(
                    url
                );
            },
            1000
        );


        /* ====================================================
           DONE
           ==================================================== */

        status(
            "ГОТОВО!\n\n" +

            "Звёзд mag < 5.0: " +
            stars.length +

            "\n\n" +

            "Создан файл:\n" +

            "celestial_globe_1500mm_24_orange_peel_mag_lt_5.svg" +

            "\n\n" +

            "Откройте SVG в Inkscape.\n\n" +

            "Размер документа:\n" +

            SVG_WIDTH.toFixed(1) +
            " × " +
            SVG_HEIGHT.toFixed(1) +
            " mm\n\n" +

            "Масштаб печати: 100%."
        );


    }
    catch(error) {

        console.error(
            error
        );


        status(
            "ОШИБКА:\n\n" +
            error.message +
            "\n\n" +
            "Проверьте, что выбран именно HYG 4.1 CSV."
        );

    }

}

</script>

</body>
</html>