Begonnen Milchstrasse in sternenhimmel einzubauen
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -15,6 +15,8 @@ $constellationDataError = null;
|
||||
$starDataError = null;
|
||||
$messierData = [];
|
||||
$messierDataError = null;
|
||||
$milkyWayData = null;
|
||||
$milkyWayDataError = null;
|
||||
$availableLocations = [];
|
||||
$starDbMagLimit = 12.0;
|
||||
|
||||
@@ -234,6 +236,16 @@ try {
|
||||
$messierDataError = $e->getMessage();
|
||||
}
|
||||
|
||||
try {
|
||||
$milkyWayPath = __DIR__ . '/js/mw.json';
|
||||
if (is_file($milkyWayPath)) {
|
||||
$milkyWayData = json_decode((string) file_get_contents($milkyWayPath), true, 512, JSON_THROW_ON_ERROR);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$milkyWayData = null;
|
||||
$milkyWayDataError = $e->getMessage();
|
||||
}
|
||||
|
||||
$favCometDataJson = json_encode(
|
||||
$favCometData ?? [],
|
||||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
|
||||
@@ -259,6 +271,14 @@ $messierDataErrorJson = json_encode(
|
||||
$messierDataError,
|
||||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
|
||||
);
|
||||
$milkyWayDataJson = json_encode(
|
||||
$milkyWayData,
|
||||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
|
||||
);
|
||||
$milkyWayDataErrorJson = json_encode(
|
||||
$milkyWayDataError,
|
||||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
|
||||
);
|
||||
?>
|
||||
<?php require __DIR__ . '/header.php'; ?>
|
||||
<?php
|
||||
@@ -373,6 +393,13 @@ if (!$availableLocations) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sp-row">
|
||||
<span class="sp-label">Milchstraße</span>
|
||||
<div class="sp-toggles">
|
||||
<label class="sp-toggle"><input type="checkbox" id="cfgMilkyWayShow" checked><span class="sp-pill"></span><span class="sp-text">an</span></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sp-row">
|
||||
<span class="sp-label">Sternbilder</span>
|
||||
<div class="sp-toggles">
|
||||
@@ -495,6 +522,14 @@ const CFG_DEFAULT = {
|
||||
ecliptic: {
|
||||
color: 'rgba(255,175,35,0.52)', width: 1.1, dash: [5,5]
|
||||
},
|
||||
milkyWay: {
|
||||
fillSoft: 'rgba(180, 208, 255, 0.018)',
|
||||
fillMain: 'rgba(210, 228, 255, 0.028)',
|
||||
stroke: 'rgba(220, 235, 255, 0.035)',
|
||||
glow: 'rgba(185, 210, 255, 0.10)',
|
||||
blur: 16,
|
||||
minPoints: 20
|
||||
},
|
||||
constellations: {
|
||||
color: 'rgba(100,160,220,0.38)', width: 0.8, dash: [],
|
||||
colorLabels: 'rgba(120,175,230,0.50)', fontLabels: '9px Rajdhani, sans-serif'
|
||||
@@ -596,6 +631,7 @@ let showAltaz = false;
|
||||
let showPlanets = true;
|
||||
let showSunMoon = true;
|
||||
let showEclip = true;
|
||||
let showMilkyWay = true;
|
||||
let showMessier = true;
|
||||
let showConst = true;
|
||||
let showComets = true;
|
||||
@@ -618,6 +654,8 @@ const STAR_DATA_URL = 'sternenhimmel.php?stars_api=1';
|
||||
const EMBEDDED_STAR_ERROR = <?php echo $starDataErrorJson ?: 'null'; ?>;
|
||||
const EMBEDDED_MESSIER_DATA = <?php echo $messierDataJson ?: '[]'; ?>;
|
||||
const EMBEDDED_MESSIER_ERROR = <?php echo $messierDataErrorJson ?: 'null'; ?>;
|
||||
const EMBEDDED_MILKY_WAY_DATA = <?php echo $milkyWayDataJson ?: 'null'; ?>;
|
||||
const EMBEDDED_MILKY_WAY_ERROR = <?php echo $milkyWayDataErrorJson ?: 'null'; ?>;
|
||||
const MESSIER = Array.isArray(EMBEDDED_MESSIER_DATA) ? EMBEDDED_MESSIER_DATA : [];
|
||||
const FAV_COMETS = <?php echo $favCometDataJson ?: '[]'; ?>;
|
||||
let constellationData = EMBEDDED_CONSTELLATION_DATA;
|
||||
@@ -636,6 +674,66 @@ function buildMStyle() {
|
||||
}
|
||||
let M_STYLE = buildMStyle();
|
||||
|
||||
function buildMilkyWayRings(data) {
|
||||
if (!data || !Array.isArray(data.features)) return [];
|
||||
|
||||
const rings = [];
|
||||
for (const feature of data.features) {
|
||||
const geometry = feature && feature.geometry ? feature.geometry : null;
|
||||
if (!geometry) continue;
|
||||
|
||||
const polygons = geometry.type === 'Polygon'
|
||||
? [geometry.coordinates]
|
||||
: geometry.type === 'MultiPolygon'
|
||||
? geometry.coordinates
|
||||
: [];
|
||||
|
||||
for (const polygon of polygons) {
|
||||
if (!Array.isArray(polygon)) continue;
|
||||
for (const rawRing of polygon) {
|
||||
if (!Array.isArray(rawRing) || rawRing.length < CFG.milkyWay.minPoints) continue;
|
||||
|
||||
const coords = [];
|
||||
let area = 0;
|
||||
|
||||
for (let i = 0; i < rawRing.length; i++) {
|
||||
const point = rawRing[i];
|
||||
if (!Array.isArray(point) || point.length < 2) continue;
|
||||
const lon = Number(point[0]);
|
||||
const lat = Number(point[1]);
|
||||
if (!Number.isFinite(lon) || !Number.isFinite(lat)) continue;
|
||||
|
||||
coords.push([lon, lat]);
|
||||
|
||||
if (i < rawRing.length - 1) {
|
||||
const next = rawRing[i + 1];
|
||||
const lon2 = Array.isArray(next) ? Number(next[0]) : NaN;
|
||||
const lat2 = Array.isArray(next) ? Number(next[1]) : NaN;
|
||||
if (Number.isFinite(lon2) && Number.isFinite(lat2)) {
|
||||
area += (lon * lat2) - (lon2 * lat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (coords.length < CFG.milkyWay.minPoints) continue;
|
||||
|
||||
const areaAbs = Math.abs(area / 2);
|
||||
if (!Number.isFinite(areaAbs)) continue;
|
||||
|
||||
rings.push({
|
||||
coords,
|
||||
areaAbs,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rings.sort((a, b) => b.areaAbs - a.areaAbs);
|
||||
return rings;
|
||||
}
|
||||
|
||||
const MILKY_WAY_RINGS = buildMilkyWayRings(EMBEDDED_MILKY_WAY_DATA);
|
||||
|
||||
function getTime() { return liveMode ? new Date() : simTime; }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -813,6 +911,11 @@ canvas.addEventListener('touchend', () => { lastTouchDist = null; });
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
function project(altDeg, azDeg) {
|
||||
if (altDeg < -0.5) return null;
|
||||
if (!isFinite(altDeg) || !isFinite(azDeg)) return null;
|
||||
return projectFullSky(altDeg, azDeg);
|
||||
}
|
||||
|
||||
function projectFullSky(altDeg, azDeg) {
|
||||
if (!isFinite(altDeg) || !isFinite(azDeg)) return null;
|
||||
const rho = (90.0 - altDeg) / 90.0;
|
||||
const azR = azDeg * Math.PI / 180.0;
|
||||
@@ -822,6 +925,11 @@ function project(altDeg, azDeg) {
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
function projectClampedToHorizon(altDeg, azDeg) {
|
||||
if (!isFinite(altDeg) || !isFinite(azDeg)) return null;
|
||||
return projectFullSky(Math.max(0, altDeg), azDeg);
|
||||
}
|
||||
|
||||
function createSafeRadialGradient(x0, y0, r0, x1, y1, r1) {
|
||||
const values = [x0, y0, r0, x1, y1, r1];
|
||||
if (!values.every(Number.isFinite)) return null;
|
||||
@@ -1161,6 +1269,105 @@ function drawEcliptic(date) {
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
|
||||
function drawMilkyWay(date) {
|
||||
if (!MILKY_WAY_RINGS.length) return;
|
||||
|
||||
const buildPath = (ring) => {
|
||||
// Clip polygon segments to the horizon (alt >= 0).
|
||||
// When a segment crosses alt=0, interpolate the exact crossing point so that
|
||||
// below-horizon projections (which can land far outside the circle and corrupt
|
||||
// the fill winding-number calculation) never enter the path.
|
||||
let subpathOpen = false;
|
||||
let hasAny = false;
|
||||
let prevPt = null;
|
||||
let prevAlt = null;
|
||||
|
||||
for (const coord of ring.coords) {
|
||||
const raDeg = ((coord[0] % 360) + 360) % 360;
|
||||
const decDeg = coord[1];
|
||||
const hor = eqHorizon(raDeg / 15, decDeg, date);
|
||||
const alt = hor.altitude;
|
||||
const currPt = projectFullSky(alt, hor.azimuth);
|
||||
|
||||
if (!currPt || !isFinite(alt)) {
|
||||
if (subpathOpen) { ctx.closePath(); subpathOpen = false; }
|
||||
prevPt = null; prevAlt = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (prevPt === null) {
|
||||
if (alt >= 0) {
|
||||
ctx.moveTo(currPt.x, currPt.y);
|
||||
subpathOpen = true; hasAny = true;
|
||||
}
|
||||
} else {
|
||||
const in0 = prevAlt >= 0;
|
||||
const in1 = alt >= 0;
|
||||
|
||||
if (in0 && in1) {
|
||||
ctx.lineTo(currPt.x, currPt.y);
|
||||
} else if (in0 && !in1) {
|
||||
// Exiting horizon: draw to the interpolated crossing, then close subpath.
|
||||
const t = prevAlt / (prevAlt - alt);
|
||||
ctx.lineTo(prevPt.x + t * (currPt.x - prevPt.x), prevPt.y + t * (currPt.y - prevPt.y));
|
||||
ctx.closePath(); subpathOpen = false;
|
||||
} else if (!in0 && in1) {
|
||||
// Entering horizon: start new subpath at the interpolated crossing.
|
||||
if (subpathOpen) { ctx.closePath(); subpathOpen = false; }
|
||||
const t = prevAlt / (prevAlt - alt);
|
||||
ctx.moveTo(prevPt.x + t * (currPt.x - prevPt.x), prevPt.y + t * (currPt.y - prevPt.y));
|
||||
ctx.lineTo(currPt.x, currPt.y);
|
||||
subpathOpen = true; hasAny = true;
|
||||
}
|
||||
// both below horizon: skip
|
||||
}
|
||||
|
||||
prevPt = currPt;
|
||||
prevAlt = alt;
|
||||
}
|
||||
|
||||
if (subpathOpen) ctx.closePath();
|
||||
return hasAny;
|
||||
};
|
||||
|
||||
const fillPass = (baseColor, withBlur) => {
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = 'screen';
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.lineCap = 'round';
|
||||
ctx.shadowBlur = withBlur ? CFG.milkyWay.blur : 0;
|
||||
ctx.shadowColor = CFG.milkyWay.glow;
|
||||
|
||||
for (const ring of MILKY_WAY_RINGS) {
|
||||
const weight = Math.max(0.45, Math.min(1, Math.log10(ring.areaAbs + 10) / 3));
|
||||
const alpha = withBlur
|
||||
? (0.006 + weight * 0.012)
|
||||
: (0.008 + weight * 0.016);
|
||||
|
||||
ctx.beginPath();
|
||||
if (!buildPath(ring)) continue;
|
||||
ctx.fillStyle = baseColor.replace('__ALPHA__', alpha.toFixed(3));
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
fillPass('rgba(180, 208, 255, __ALPHA__)', true);
|
||||
fillPass('rgba(210, 228, 255, __ALPHA__)', false);
|
||||
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = 'screen';
|
||||
ctx.strokeStyle = CFG.milkyWay.stroke;
|
||||
ctx.lineWidth = 0.7;
|
||||
for (const ring of MILKY_WAY_RINGS) {
|
||||
ctx.beginPath();
|
||||
if (!buildPath(ring)) continue;
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// LAYER: HÖHEN-/AZIMUT-GITTER (rein geometrisch, kein Sternbezug)
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -1761,6 +1968,7 @@ function render() {
|
||||
ctx.fill();
|
||||
ctx.clip();
|
||||
|
||||
if (showMilkyWay) drawMilkyWay(date);
|
||||
if (showAltaz) drawAltazGrid();
|
||||
if (showEq) drawEquatorialGrid(date);
|
||||
if (showEclip) drawEcliptic(date);
|
||||
@@ -2095,6 +2303,7 @@ spBind('cfgPlanetsShow', () => showPlanets, v => showPlanets = v);
|
||||
spBind('cfgCometsShow', () => showComets, v => showComets = v);
|
||||
spBind('cfgCometsLabels', () => labelComets, v => labelComets = v);
|
||||
spBind('cfgEclipShow', () => showEclip, v => showEclip = v);
|
||||
spBind('cfgMilkyWayShow', () => showMilkyWay, v => showMilkyWay = v);
|
||||
spBind('cfgMessierShow', () => showMessier, v => showMessier = v);
|
||||
spBind('cfgSunMoonShow', () => showSunMoon, v => showSunMoon = v);
|
||||
|
||||
@@ -2185,6 +2394,10 @@ if (EMBEDDED_MESSIER_ERROR) {
|
||||
console.warn('Messier-Daten aus der Datenbank nicht geladen:', EMBEDDED_MESSIER_ERROR);
|
||||
}
|
||||
|
||||
if (EMBEDDED_MILKY_WAY_ERROR) {
|
||||
console.warn('Milchstraßen-Daten nicht geladen:', EMBEDDED_MILKY_WAY_ERROR);
|
||||
}
|
||||
|
||||
function updateStarLoadingUi(done, total) {
|
||||
if (!starLoadingOverlay || !starLoadingFill || !starLoadingStatus || !starLoadingNote) return;
|
||||
const safeTotal = Math.max(1, total);
|
||||
|
||||
Reference in New Issue
Block a user