Загрузка данных
<!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;
}
button:hover {
background: #4b91ff;
}
#status {
margin-top: 25px;
white-space: pre-line;
font-family: monospace;
color: #bcd7ff;
}
h1 {
font-size: 28px;
}
.info {
max-width: 800px;
line-height: 1.5;
}
</style>
</head>
<body>
<h1>CELESTIAL GLOBE Ø1500 mm</h1>
<div class="info">
<p>
24 классических orange-peel лепестка.
HYG 4.1. Только звёзды с <b>mag < 5.0</b>.
</p>
<p>
Нажми кнопку ниже. Браузер скачает каталог HYG,
рассчитает положение звёзд и создаст SVG 1:1 в миллиметрах.
</p>
</div>
<button onclick="generate()">СОЗДАТЬ SVG</button>
<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;
// HYG 4.1
const HYG_URL =
"https://raw.githubusercontent.com/astronexus/HYG-Database/main/hyg/CURRENT/hyg_v41.csv";
// Звёзды
const STAR_MIN_MM = 0.8;
const STAR_MAX_MM = 4.5;
// Разрешение границ
const LAT_STEPS = 360;
// Раскладка
const COLUMNS = 6;
const ROWS = 4;
const GORE_GAP_X = 100.0;
const GORE_GAP_Y = 180.0;
const PAGE_MARGIN = 150.0;
// ============================================================
// DIMENSIONS
// ============================================================
// Максимальная ширина orange-peel gore на экваторе.
//
// Для синусоидальной orange-peel проекции:
//
// x = R * lambda * cos(phi)
//
// На экваторе:
//
// width = 2 * R * lambda
//
// lambda = 7.5 degrees
//
// ============================================================
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;
// ============================================================
// STATUS
// ============================================================
function status(text) {
document.getElementById("status").textContent = text;
}
// ============================================================
// 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)
);
}
// ============================================================
// CLASSICAL ORANGE-PEEL PROJECTION
// ============================================================
//
// latitude = -90 ... +90
// longitude = -7.5 ... +7.5
//
// x = R * lambda * cos(latitude)
// y = R * latitude
//
// ВАЖНО:
//
// ЭТА ФУНКЦИЯ ИСПОЛЬЗУЕТСЯ И ДЛЯ ГРАНИЦ,
// И ДЛЯ ЗВЁЗД.
//
// ============================================================
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() {
try {
status(
"Загружаю HYG 4.1...\n"
);
// ----------------------------------------------------
// DOWNLOAD
// ----------------------------------------------------
const response =
await fetch(HYG_URL);
if (!response.ok) {
throw new Error(
"Не удалось загрузить HYG."
);
}
const csvText =
await response.text();
status(
"HYG загружен.\n" +
"Разбираю каталог..."
);
// ----------------------------------------------------
// CSV
// ----------------------------------------------------
const rows =
parseCSV(csvText);
if (!rows.length) {
throw new Error(
"CSV пустой."
);
}
const header =
rows[0];
const index = {};
for (
let i = 0;
i < header.length;
i++
) {
index[
header[i]
] = i;
}
const stars = [];
// ----------------------------------------------------
// READ STARS
// ----------------------------------------------------
for (
let r = 1;
r < rows.length;
r++
) {
const row =
rows[r];
try {
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
//
// 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]
: ""
});
} catch(e) {
continue;
}
}
status(
"Звёзд mag < 5.0: " +
stars.length +
"\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 lines
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.push("</svg>");
// ====================================================
// DOWNLOAD SVG
// ====================================================
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
);
URL.revokeObjectURL(
url
);
// ====================================================
// DONE
// ====================================================
status(
"ГОТОВО!\n\n" +
"Звёзд mag < 5.0: " +
stars.length +
"\n\n" +
"Создан файл:\n" +
"celestial_globe_1500mm_24_orange_peel_mag_lt_5.svg\n\n" +
"Теперь открой его в Inkscape.\n\n" +
"В Inkscape:\n" +
"File → Export\n" +
"Выбери Page\n" +
"Export"
);
} catch(error) {
console.error(error);
status(
"ОШИБКА:\n\n" +
error.message +
"\n\n" +
"Если браузер заблокировал загрузку HYG,\n" +
"попробуй Chrome или Edge."
);
}
}
</script>
</body>
</html>