538 lines
15 KiB
PHP
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'; ?>
|