Files
skyview.astronomiemuseum.de/public/geocron3d_C.php
T
2026-04-09 08:56:18 +02:00

538 lines
15 KiB
PHP

<?php
declare(strict_types=1);
session_start();
$loggedIn = isset($_SESSION['user_id']);
$pageTitle = 'Geocron 3D Cesium - AstroTools';
$bodyClass = 'geocron-page geocron3d-page geocron3d-cesium-page';
$publicBasePath = './';
require __DIR__ . '/header.php';
?>
<section class="card geocron-card">
<div class="geocron-stage geocron3d-stage">
<div class="geocron3d-shell">
<div class="geocron3d-copy">
<p class="geocron3d-kicker">CesiumJS-Prototyp</p>
<h2>3D-Erde mit Satellitenbahnen in CesiumJS</h2>
<p>
Diese Variante nutzt CesiumJS statt Three.js. Die TLE-Daten kommen wie bisher aus deinem
Account, die Erde wird als Globus gerendert und die Satellitenbahnen als Orbit-Spuren gezeigt.
</p>
<label class="geocron3d-toggle">
<input type="checkbox" id="geocron3dCesiumLabelsToggle">
<span>Satelliten-Beschriftungen</span>
</label>
<div class="geocron3d-time-controls" aria-label="Zeitsteuerung">
<button type="button" class="geocron3d-time-button is-active" data-time-scale="1">Live</button>
<button type="button" class="geocron3d-time-button" data-time-scale="25">25x</button>
<button type="button" class="geocron3d-time-button" data-time-scale="60">60x</button>
<button type="button" class="geocron3d-time-button" data-time-scale="120">120x</button>
<button type="button" class="geocron3d-time-button" data-time-scale="0">Pause</button>
</div>
<div class="geocron3d-time-readout" id="geocron3dCesiumTimeReadout">Simulationszeit: --</div>
<div class="geocron3d-status" id="geocron3dCesiumStatus">Lade Cesium-Szene...</div>
</div>
<div class="geocron3d-view geocron3d-cesium-view">
<div id="geocron3dCesiumView" aria-label="3D Erde mit Satelliten in CesiumJS"></div>
</div>
</div>
</div>
</section>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/cesium@1.140.0/Build/Cesium/Widgets/widgets.css">
<style>
.geocron3d-page main > .container {
max-width: min(1720px, calc(100% - 28px));
}
.geocron3d-stage {
padding: 18px;
}
.geocron3d-shell {
display: grid;
grid-template-columns: minmax(260px, 360px) minmax(0, 1fr);
gap: 18px;
align-items: stretch;
}
.geocron3d-copy,
.geocron3d-view {
min-height: 620px;
border: 1px solid rgba(201, 168, 76, 0.14);
border-radius: calc(var(--radius) + 4px);
overflow: hidden;
}
.geocron3d-copy {
padding: 22px 20px;
background:
radial-gradient(circle at top left, rgba(74, 158, 255, 0.16), transparent 38%),
linear-gradient(180deg, rgba(10, 15, 30, 0.96) 0%, rgba(7, 11, 22, 0.98) 100%);
}
.geocron3d-kicker {
margin: 0 0 10px;
color: rgba(201, 168, 76, 0.82);
font-size: 0.74rem;
letter-spacing: 0.18em;
text-transform: uppercase;
}
.geocron3d-copy h2 {
margin: 0 0 14px;
font-size: clamp(1.5rem, 1.2rem + 1vw, 2.1rem);
}
.geocron3d-copy p {
margin: 0 0 16px;
color: rgba(222, 232, 255, 0.86);
line-height: 1.7;
}
.geocron3d-toggle {
display: inline-flex;
align-items: center;
gap: 10px;
margin: 0 0 16px;
color: rgba(222, 232, 255, 0.88);
cursor: pointer;
user-select: none;
}
.geocron3d-toggle input {
width: 16px;
height: 16px;
accent-color: #6b9dff;
}
.geocron3d-status {
display: inline-flex;
align-items: center;
min-height: 40px;
padding: 8px 12px;
border: 1px solid rgba(108, 145, 220, 0.28);
border-radius: 999px;
background: rgba(10, 16, 32, 0.72);
color: rgba(192, 214, 255, 0.92);
font-size: 0.92rem;
}
.geocron3d-time-controls {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 0 0 12px;
}
.geocron3d-time-button {
padding: 8px 12px;
border: 1px solid rgba(108, 145, 220, 0.28);
border-radius: 999px;
background: rgba(10, 16, 32, 0.72);
color: rgba(220, 232, 255, 0.92);
font: inherit;
cursor: pointer;
transition: background-color 0.16s ease, border-color 0.16s ease, transform 0.16s ease;
}
.geocron3d-time-button:hover {
border-color: rgba(125, 170, 255, 0.55);
background: rgba(18, 28, 52, 0.92);
}
.geocron3d-time-button.is-active {
border-color: rgba(150, 190, 255, 0.78);
background: rgba(38, 66, 122, 0.92);
}
.geocron3d-time-readout {
margin: 0 0 12px;
color: rgba(192, 214, 255, 0.88);
font-size: 0.9rem;
line-height: 1.5;
}
.geocron3d-view {
position: relative;
background:
radial-gradient(circle at 30% 18%, rgba(35, 60, 116, 0.35), transparent 26%),
linear-gradient(180deg, #02050b 0%, #040914 100%);
}
.geocron3d-cesium-view,
.geocron3d-cesium-view > div,
.geocron3d-cesium-view .cesium-widget,
.geocron3d-cesium-view canvas {
width: 100%;
height: 100%;
display: block;
}
.geocron3d-cesium-view .cesium-viewer-toolbar,
.geocron3d-cesium-view .cesium-viewer-animationContainer,
.geocron3d-cesium-view .cesium-viewer-timelineContainer,
.geocron3d-cesium-view .cesium-viewer-bottom {
display: none !important;
}
.geocron3d-cesium-view .cesium-viewer {
width: 100%;
height: 100%;
}
.geocron3d-cesium-view .cesium-widget-credits {
background: rgba(4, 8, 18, 0.72);
border-radius: 12px 0 0 0;
padding: 4px 8px;
}
@media (max-width: 1080px) {
.geocron3d-shell {
grid-template-columns: 1fr;
}
.geocron3d-copy,
.geocron3d-view {
min-height: 280px;
}
.geocron3d-view {
min-height: 520px;
}
}
</style>
<script>
window.CESIUM_BASE_URL = 'https://cdn.jsdelivr.net/npm/cesium@1.140.0/Build/Cesium/';
</script>
<script src="https://cdn.jsdelivr.net/npm/cesium@1.140.0/Build/Cesium/Cesium.js"></script>
<script>
const ORBIT_WINDOW_MINUTES = 90;
const ORBIT_STEP_SECONDS = 20;
const ORBIT_REFRESH_MS = 30_000;
const statusEl = document.getElementById('geocron3dCesiumStatus');
const labelsToggle = document.getElementById('geocron3dCesiumLabelsToggle');
const timeReadoutEl = document.getElementById('geocron3dCesiumTimeReadout');
const timeButtons = Array.from(document.querySelectorAll('[data-time-scale]'));
let labelsEnabled = false;
let timeScale = 1;
let satelliteLib = null;
let satelliteItems = [];
let nextOrbitRefreshAt = 0;
let viewer = null;
function setStatus(text, isError = false) {
statusEl.textContent = text;
statusEl.style.color = isError ? 'rgba(255, 196, 196, 0.95)' : 'rgba(192, 214, 255, 0.92)';
statusEl.style.borderColor = isError ? 'rgba(190, 80, 80, 0.34)' : 'rgba(108, 145, 220, 0.28)';
}
function loadSatelliteJs() {
return new Promise((resolve, reject) => {
if (window.satellite) {
resolve(window.satellite);
return;
}
const existing = document.querySelector('script[data-satellite-js="true"]');
if (existing) {
existing.addEventListener('load', () => resolve(window.satellite), { once: true });
existing.addEventListener('error', () => reject(new Error('satellite.js konnte nicht geladen werden.')), { once: true });
return;
}
const script = document.createElement('script');
script.src = 'https://unpkg.com/satellite.js@6.0.2/dist/satellite.min.js';
script.async = true;
script.dataset.satelliteJs = 'true';
script.onload = () => resolve(window.satellite);
script.onerror = () => reject(new Error('satellite.js konnte nicht geladen werden.'));
document.head.appendChild(script);
});
}
function wrapLongitude(lon) {
return ((lon + 180) % 360 + 360) % 360 - 180;
}
function formatSimulationTime(date) {
return new Intl.DateTimeFormat('de-DE', {
dateStyle: 'medium',
timeStyle: 'medium',
timeZone: 'UTC',
}).format(date) + ' UTC';
}
function createViewer() {
return Cesium.TileMapServiceImageryProvider.fromUrl(
Cesium.buildModuleUrl('Assets/Textures/NaturalEarthII')
).then((imageryProvider) => {
const nextViewer = new Cesium.Viewer('geocron3dCesiumView', {
animation: false,
baseLayer: Cesium.ImageryLayer.fromProviderAsync(Promise.resolve(imageryProvider)),
baseLayerPicker: false,
fullscreenButton: false,
geocoder: false,
homeButton: false,
infoBox: false,
navigationHelpButton: false,
sceneModePicker: false,
selectionIndicator: false,
timeline: false,
shouldAnimate: true,
});
nextViewer.scene.backgroundColor = Cesium.Color.BLACK;
nextViewer.scene.globe.enableLighting = true;
nextViewer.scene.globe.showGroundAtmosphere = true;
nextViewer.scene.skyAtmosphere.show = true;
nextViewer.scene.moon.show = false;
nextViewer.clock.clockRange = Cesium.ClockRange.UNBOUNDED;
nextViewer.clock.multiplier = 1;
nextViewer.clock.shouldAnimate = true;
nextViewer.scene.screenSpaceCameraController.minimumZoomDistance = 7_500_000;
nextViewer.scene.screenSpaceCameraController.maximumZoomDistance = 80_000_000;
nextViewer.camera.setView({
destination: Cesium.Cartesian3.fromDegrees(12, 24, 18_000_000),
});
return nextViewer;
});
}
function getCurrentSimulationDate() {
return Cesium.JulianDate.toDate(viewer.clock.currentTime);
}
function updateTimeReadout() {
const currentTime = getCurrentSimulationDate();
const modeLabel = timeScale === 0 ? 'Pausiert' : (timeScale === 1 ? 'Live' : `${timeScale}x`);
timeReadoutEl.textContent = `Simulationszeit: ${formatSimulationTime(currentTime)} (${modeLabel})`;
}
function buildOrbitSamples(satrec, now) {
const samples = [];
const halfWindowMs = (ORBIT_WINDOW_MINUTES * 60 * 1000) / 2;
for (let offsetMs = -halfWindowMs; offsetMs <= halfWindowMs; offsetMs += ORBIT_STEP_SECONDS * 1000) {
const date = new Date(now.getTime() + offsetMs);
const propagation = satelliteLib.propagate(satrec, date);
if (!propagation.position) {
continue;
}
const gmst = satelliteLib.gstime(date);
const geodetic = satelliteLib.eciToGeodetic(propagation.position, gmst);
samples.push({
date,
lat: satelliteLib.degreesLat(geodetic.latitude),
lon: wrapLongitude(satelliteLib.degreesLong(geodetic.longitude)),
altMeters: geodetic.height * 1000,
});
}
return samples;
}
function createPositionProperty(samples) {
const property = new Cesium.SampledPositionProperty();
for (const sample of samples) {
property.addSample(
Cesium.JulianDate.fromDate(sample.date),
Cesium.Cartesian3.fromDegrees(sample.lon, sample.lat, sample.altMeters)
);
}
property.setInterpolationOptions({
interpolationDegree: 1,
interpolationAlgorithm: Cesium.LinearApproximation,
});
return property;
}
function createPolylinePositions(samples) {
const flattened = [];
for (const sample of samples) {
flattened.push(sample.lon, sample.lat, sample.altMeters);
}
return Cesium.Cartesian3.fromDegreesArrayHeights(flattened);
}
function updateLabelVisibility() {
for (const item of satelliteItems) {
if (item.entity.label) {
item.entity.label.show = labelsEnabled;
}
}
}
function refreshSatelliteTracks(now) {
for (const item of satelliteItems) {
const samples = buildOrbitSamples(item.satrec, now);
if (samples.length < 2) {
item.entity.show = false;
item.orbitEntity.show = false;
continue;
}
item.entity.show = true;
item.entity.position = createPositionProperty(samples);
item.entity.orientation = new Cesium.VelocityOrientationProperty(item.entity.position);
item.orbitEntity.show = true;
item.orbitEntity.polyline.positions = createPolylinePositions(samples);
}
}
function setTimeScale(nextScale) {
timeScale = nextScale;
if (nextScale === 1) {
viewer.clock.currentTime = Cesium.JulianDate.fromDate(new Date());
viewer.clock.multiplier = 1;
viewer.clock.shouldAnimate = true;
refreshSatelliteTracks(getCurrentSimulationDate());
nextOrbitRefreshAt = performance.now() + ORBIT_REFRESH_MS;
} else if (nextScale === 0) {
viewer.clock.shouldAnimate = false;
} else {
viewer.clock.multiplier = nextScale;
viewer.clock.shouldAnimate = true;
}
for (const button of timeButtons) {
button.classList.toggle('is-active', Number(button.dataset.timeScale) === nextScale);
}
updateTimeReadout();
}
function createSatelliteEntity(row, index, now) {
const satrec = satelliteLib.twoline2satrec(row.tle_line1, row.tle_line2);
const samples = buildOrbitSamples(satrec, now);
const hue = (index * 0.173) % 1;
const color = Cesium.Color.fromHsl(hue, 0.82, 0.64, 1);
if (samples.length < 2) {
return null;
}
const position = createPositionProperty(samples);
const entity = viewer.entities.add({
position,
orientation: new Cesium.VelocityOrientationProperty(position),
label: {
text: row.object_name ?? String(row.norad_cat_id),
show: labelsEnabled,
font: '12px sans-serif',
fillColor: Cesium.Color.WHITE,
outlineColor: Cesium.Color.BLACK.withAlpha(0.9),
outlineWidth: 2,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
pixelOffset: new Cesium.Cartesian2(0, -18),
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
},
point: {
pixelSize: Number(row.norad_cat_id) === 25544 ? 11 : 8,
color,
outlineColor: Cesium.Color.WHITE.withAlpha(0.9),
outlineWidth: 1,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
},
});
const orbitEntity = viewer.entities.add({
polyline: {
positions: createPolylinePositions(samples),
width: 2,
material: color.withAlpha(0.55),
},
});
return { satrec, entity, orbitEntity };
}
async function init() {
try {
updateTimeReadout();
satelliteLib = await loadSatelliteJs();
setStatus('Lade TLE-Daten und baue Cesium-Globus auf...');
const response = await fetch('./geocron/getTLE.php', { cache: 'no-store' });
const rows = await response.json();
if (!response.ok) {
throw new Error(rows.error || `TLE-Fehler (${response.status})`);
}
viewer.entities.removeAll();
const now = getCurrentSimulationDate();
satelliteItems = rows
.map((row, index) => createSatelliteEntity(row, index, now))
.filter(Boolean);
nextOrbitRefreshAt = performance.now() + ORBIT_REFRESH_MS;
if (satelliteItems.length === 0) {
setStatus('Keine Satelliten im Konto gefunden. Bitte zuerst Favoriten im Satellitenbereich hinterlegen.');
} else {
setStatus(`${satelliteItems.length} Satelliten im Orbit geladen.`);
}
} catch (error) {
console.error(error);
setStatus(error.message || 'Cesium-Szene konnte nicht geladen werden.', true);
}
}
window.addEventListener('beforeunload', () => {
if (viewer && !viewer.isDestroyed()) {
viewer.destroy();
}
});
(async () => {
viewer = await createViewer();
labelsToggle.addEventListener('change', () => {
labelsEnabled = labelsToggle.checked;
updateLabelVisibility();
});
for (const button of timeButtons) {
button.addEventListener('click', () => {
setTimeScale(Number(button.dataset.timeScale));
});
}
viewer.clock.onTick.addEventListener(() => {
updateTimeReadout();
if (!satelliteLib || satelliteItems.length === 0) {
return;
}
if (performance.now() >= nextOrbitRefreshAt) {
refreshSatelliteTracks(getCurrentSimulationDate());
nextOrbitRefreshAt = performance.now() + ORBIT_REFRESH_MS;
}
});
init();
})().catch((error) => {
console.error(error);
setStatus(error.message || 'Cesium-Szene konnte nicht geladen werden.', true);
});
</script>
<?php require __DIR__ . '/footer.php'; ?>