Загрузка данных
<!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>