Neue Features: Satelliten-Orbits, Naturkatastrophen, Visual Modes
SATELLITEN (CelesTrak TLE): - Raumstationen (ISS), GPS, Galileo, Wetter, Erdbeobachtung, Starlink - Echtzeit-Positionsberechnung aus Kepler-Elementen (2s Update) - Orbitbahnen als leuchtende Linien (Stationen, GPS, Galileo) - Farbkodiert nach Gruppe (rot=Station, orange=GPS, blau=Galileo) NATURKATASTROPHEN (NASA EONET): - Waldbraende, Vulkane, Stuerme, Fluten, Erdrutsche, Eis - Farbige Punkte mit Emoji-Labels - Klick zeigt Details und Quellen VISUAL MODES: - STD: Standard-Ansicht - NVG: Nachtsicht (gruener Monochrom-Filter) - FLIR: Thermal-Ansicht (invertiert, Infrarot-Look) - CRT: Retro-Monitor (Scanlines, Vignette) 4 neue Dateien: satellites.js, disasters.js, visualmodes.js, data_satellites.py, data_disasters.py
Dieser Commit ist enthalten in:
30
src/data_disasters.py
Normale Datei
30
src/data_disasters.py
Normale Datei
@@ -0,0 +1,30 @@
|
||||
"""Naturkatastrophen: NASA EONET (Earth Observatory Natural Event Tracker)."""
|
||||
import logging
|
||||
import time
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter
|
||||
|
||||
logger = logging.getLogger("globe.disasters")
|
||||
router = APIRouter()
|
||||
|
||||
_cache: dict = {"data": None, "ts": 0}
|
||||
|
||||
|
||||
@router.get("/disasters")
|
||||
async def get_disasters():
|
||||
now = time.time()
|
||||
if _cache["data"] and now - _cache["ts"] < 600: # 10min Cache
|
||||
return _cache["data"]
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=12) as client:
|
||||
r = await client.get("https://eonet.gsfc.nasa.gov/api/v3/events?status=open&limit=100")
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
_cache["data"] = data
|
||||
_cache["ts"] = time.time()
|
||||
logger.info(f"Disasters: {len(data.get('events', []))} aktive Ereignisse")
|
||||
return data
|
||||
except Exception as e:
|
||||
logger.warning(f"NASA EONET Fehler: {e}")
|
||||
return _cache["data"] or {"events": []}
|
||||
69
src/data_satellites.py
Normale Datei
69
src/data_satellites.py
Normale Datei
@@ -0,0 +1,69 @@
|
||||
"""Satelliten-Daten: CelesTrak TLE Orbital Elements."""
|
||||
import logging
|
||||
import time
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter
|
||||
|
||||
logger = logging.getLogger("globe.satellites")
|
||||
router = APIRouter()
|
||||
|
||||
_cache: dict = {"data": None, "ts": 0}
|
||||
|
||||
# Wichtigste Satellitengruppen (nicht alle 14.000)
|
||||
_GROUPS = [
|
||||
("stations", "Raumstationen (ISS etc.)"),
|
||||
("gps-ops", "GPS Navigationssatelliten"),
|
||||
("galileo", "Galileo Navigation"),
|
||||
("weather", "Wettersatelliten"),
|
||||
("resource", "Erdbeobachtung"),
|
||||
("starlink", "Starlink (Auswahl)"),
|
||||
("active", None), # Fallback: alle aktiven, wird gefiltert
|
||||
]
|
||||
|
||||
|
||||
@router.get("/satellites")
|
||||
async def get_satellites():
|
||||
now = time.time()
|
||||
if _cache["data"] and now - _cache["ts"] < 3600: # 1h Cache
|
||||
return _cache["data"]
|
||||
|
||||
all_sats = []
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=15) as client:
|
||||
for group, label in _GROUPS:
|
||||
if group == "active":
|
||||
continue # Zu gross, skip
|
||||
url = f"https://celestrak.org/NORAD/elements/gp.php?GROUP={group}&FORMAT=json"
|
||||
try:
|
||||
r = await client.get(url)
|
||||
if r.status_code == 200:
|
||||
sats = r.json()
|
||||
# Starlink: nur 200 nehmen (sind tausende)
|
||||
if group == "starlink":
|
||||
sats = sats[:200]
|
||||
for s in sats:
|
||||
all_sats.append({
|
||||
"name": s.get("OBJECT_NAME", "?"),
|
||||
"id": s.get("NORAD_CAT_ID"),
|
||||
"group": group,
|
||||
"epoch": s.get("EPOCH"),
|
||||
"meanMotion": s.get("MEAN_MOTION"),
|
||||
"eccentricity": s.get("ECCENTRICITY"),
|
||||
"inclination": s.get("INCLINATION"),
|
||||
"raOfAscNode": s.get("RA_OF_ASC_NODE"),
|
||||
"argOfPericenter": s.get("ARG_OF_PERICENTER"),
|
||||
"meanAnomaly": s.get("MEAN_ANOMALY"),
|
||||
"bstar": s.get("BSTAR"),
|
||||
"meanMotionDot": s.get("MEAN_MOTION_DOT"),
|
||||
})
|
||||
except Exception as e:
|
||||
logger.warning(f"CelesTrak {group}: {e}")
|
||||
except Exception as e:
|
||||
logger.warning(f"CelesTrak Fehler: {e}")
|
||||
return _cache["data"] or {"satellites": [], "total": 0}
|
||||
|
||||
_cache["data"] = {"satellites": all_sats, "total": len(all_sats)}
|
||||
_cache["ts"] = time.time()
|
||||
logger.info(f"Satellites: {len(all_sats)} geladen")
|
||||
return _cache["data"]
|
||||
@@ -31,12 +31,16 @@ from data_flights import router as flights_router, start_flight_collector
|
||||
from data_ships import router as ships_router, start_ais_collector
|
||||
from data_quakes import router as quakes_router
|
||||
from data_gdelt import router as gdelt_router
|
||||
from data_satellites import router as satellites_router
|
||||
from data_disasters import router as disasters_router
|
||||
|
||||
# Alle Daten-APIs hinter Auth
|
||||
app.include_router(flights_router, prefix="/api", dependencies=[Depends(get_current_user)])
|
||||
app.include_router(ships_router, prefix="/api", dependencies=[Depends(get_current_user)])
|
||||
app.include_router(quakes_router, prefix="/api", dependencies=[Depends(get_current_user)])
|
||||
app.include_router(gdelt_router, prefix="/api", dependencies=[Depends(get_current_user)])
|
||||
app.include_router(satellites_router, prefix="/api", dependencies=[Depends(get_current_user)])
|
||||
app.include_router(disasters_router, prefix="/api", dependencies=[Depends(get_current_user)])
|
||||
|
||||
# --- Static files ---
|
||||
static_dir = Path(__file__).parent.parent / "static"
|
||||
|
||||
@@ -149,3 +149,84 @@ html, body { height: 100%; overflow: hidden; background: var(--bg-primary); colo
|
||||
.layer-status.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
/* === Neue Layer-Dots === */
|
||||
.dot-satellites { background: #ff4444; }
|
||||
.dot-disasters { background: #ff8800; }
|
||||
|
||||
/* === Visual Mode Buttons === */
|
||||
.vmode-btn {
|
||||
padding: 4px 8px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 3px;
|
||||
color: var(--text-dim);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.vmode-btn:hover { border-color: var(--accent); color: var(--text); }
|
||||
.vmode-btn.active { background: rgba(0,255,136,0.15); border-color: var(--accent); color: var(--accent); }
|
||||
|
||||
/* === Visual Overlay (CRT Scanlines) === */
|
||||
.visual-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
pointer-events: none;
|
||||
}
|
||||
.vmode-crt-overlay {
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent,
|
||||
transparent 2px,
|
||||
rgba(0, 0, 0, 0.08) 2px,
|
||||
rgba(0, 0, 0, 0.08) 4px
|
||||
);
|
||||
box-shadow: inset 0 0 120px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* === Night Vision Mode === */
|
||||
.vmode-nvg {
|
||||
filter: saturate(0) brightness(1.2) contrast(1.1);
|
||||
}
|
||||
.vmode-nvg .cesium-viewer { mix-blend-mode: normal; }
|
||||
.vmode-nvg::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 255, 0, 0.08);
|
||||
pointer-events: none;
|
||||
z-index: 40;
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
||||
/* === FLIR Thermal Mode === */
|
||||
.vmode-flir {
|
||||
filter: saturate(0) invert(1) contrast(1.3) brightness(0.9);
|
||||
}
|
||||
.vmode-flir::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, rgba(255,100,0,0.05), rgba(255,0,100,0.05));
|
||||
pointer-events: none;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
/* === CRT Mode === */
|
||||
.vmode-crt {
|
||||
filter: contrast(1.1) brightness(0.95);
|
||||
}
|
||||
.vmode-crt::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse at center, transparent 60%, rgba(0,0,0,0.4) 100%);
|
||||
pointer-events: none;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
@@ -78,6 +78,22 @@
|
||||
</label>
|
||||
<div class="layer-loading" id="loading-gdelt"></div>
|
||||
<div class="layer-status" id="status-gdelt"></div>
|
||||
<label class="layer-toggle">
|
||||
<input type="checkbox" id="layer-satellites">
|
||||
<span class="layer-dot dot-satellites"></span>
|
||||
<span class="layer-name">Satelliten</span>
|
||||
<span class="layer-count" id="count-satellites">-</span>
|
||||
</label>
|
||||
<div class="layer-loading" id="loading-satellites"></div>
|
||||
<div class="layer-status" id="status-satellites"></div>
|
||||
<label class="layer-toggle">
|
||||
<input type="checkbox" id="layer-disasters">
|
||||
<span class="layer-dot dot-disasters"></span>
|
||||
<span class="layer-name">Katastrophen</span>
|
||||
<span class="layer-count" id="count-disasters">-</span>
|
||||
</label>
|
||||
<div class="layer-loading" id="loading-disasters"></div>
|
||||
<div class="layer-status" id="status-disasters"></div>
|
||||
</div>
|
||||
<div class="panel-divider"></div>
|
||||
<div class="panel-section">
|
||||
@@ -93,6 +109,17 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="panel-divider"></div>
|
||||
<div class="panel-divider"></div>
|
||||
<div class="panel-section">
|
||||
<div style="font-size:9px;letter-spacing:1.5px;color:var(--accent);margin-bottom:6px;">ANSICHT</div>
|
||||
<div style="display:flex;gap:4px;flex-wrap:wrap;">
|
||||
<button class="vmode-btn active" data-mode="standard" onclick="VisualModes.set('standard')">STD</button>
|
||||
<button class="vmode-btn" data-mode="nvg" onclick="VisualModes.set('nvg')">NVG</button>
|
||||
<button class="vmode-btn" data-mode="flir" onclick="VisualModes.set('flir')">FLIR</button>
|
||||
<button class="vmode-btn" data-mode="crt" onclick="VisualModes.set('crt')">CRT</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-divider"></div>
|
||||
<div class="panel-coords" id="coords-display">
|
||||
LAT: -- LON: --
|
||||
</div>
|
||||
@@ -100,6 +127,7 @@
|
||||
|
||||
<!-- CesiumJS Container -->
|
||||
<div id="cesiumContainer"></div>
|
||||
<div id="visual-overlay" class="visual-overlay" style="display:none"></div>
|
||||
|
||||
<!-- Bottom Bar -->
|
||||
<footer id="bottom-bar">
|
||||
@@ -114,5 +142,8 @@
|
||||
<script src="/static/js/layers/ships.js"></script>
|
||||
<script src="/static/js/layers/quakes.js"></script>
|
||||
<script src="/static/js/layers/gdelt.js"></script>
|
||||
<script src="/static/js/layers/satellites.js"></script>
|
||||
<script src="/static/js/layers/disasters.js"></script>
|
||||
<script src="/static/js/layers/visualmodes.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -81,6 +81,7 @@ const Globe = {
|
||||
// Layer starten (die mit checked)
|
||||
|
||||
this._toggleLabels(true);
|
||||
if (typeof VisualModes !== 'undefined') VisualModes.init();
|
||||
document.getElementById('bottom-stats').textContent = 'Globe initialisiert — Lade Daten...';
|
||||
},
|
||||
|
||||
@@ -111,6 +112,8 @@ const Globe = {
|
||||
'layer-ships': function(on) { on ? ShipsLayer.start(Globe.viewer) : ShipsLayer.stop(); },
|
||||
'layer-quakes': function(on) { on ? QuakesLayer.start(Globe.viewer) : QuakesLayer.stop(); },
|
||||
'layer-gdelt': function(on) { on ? GdeltLayer.start(Globe.viewer) : GdeltLayer.stop(); },
|
||||
'layer-satellites': function(on) { on ? SatellitesLayer.start(Globe.viewer) : SatellitesLayer.stop(); },
|
||||
'layer-disasters': function(on) { on ? DisastersLayer.start(Globe.viewer) : DisastersLayer.stop(); },
|
||||
'layer-daynight': function(on) { Globe.viewer.scene.globe.enableLighting = on; },
|
||||
'layer-labels': function(on) { Globe._toggleLabels(on); },
|
||||
};
|
||||
@@ -133,6 +136,12 @@ const Globe = {
|
||||
if (typeof QuakesLayer !== 'undefined' && QuakesLayer._count > 0) {
|
||||
document.getElementById('count-quakes').textContent = QuakesLayer._count.toLocaleString('de-DE');
|
||||
}
|
||||
if (typeof SatellitesLayer !== 'undefined' && SatellitesLayer._count > 0) {
|
||||
document.getElementById('count-satellites').textContent = SatellitesLayer._count.toLocaleString('de-DE');
|
||||
}
|
||||
if (typeof DisastersLayer !== 'undefined' && DisastersLayer._count > 0) {
|
||||
document.getElementById('count-disasters').textContent = DisastersLayer._count.toLocaleString('de-DE');
|
||||
}
|
||||
if (typeof GdeltLayer !== 'undefined' && GdeltLayer._count > 0) {
|
||||
document.getElementById('count-gdelt').textContent = GdeltLayer._count.toLocaleString('de-DE');
|
||||
}
|
||||
|
||||
86
static/js/layers/disasters.js
Normale Datei
86
static/js/layers/disasters.js
Normale Datei
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Naturkatastrophen-Layer: NASA EONET (Waldbrände, Vulkane, Stürme, Eis).
|
||||
*/
|
||||
const DisastersLayer = {
|
||||
_viewer: null,
|
||||
_dataSource: null,
|
||||
_interval: null,
|
||||
_count: 0,
|
||||
|
||||
start(viewer) {
|
||||
if (this._dataSource) return;
|
||||
this._viewer = viewer;
|
||||
this._dataSource = new Cesium.CustomDataSource('disasters');
|
||||
viewer.dataSources.add(this._dataSource);
|
||||
this._fetch();
|
||||
var self = this;
|
||||
this._interval = setInterval(function() { self._fetch(); }, 600000);
|
||||
},
|
||||
|
||||
stop() {
|
||||
if (this._interval) { clearInterval(this._interval); this._interval = null; }
|
||||
if (this._dataSource && this._viewer) { this._viewer.dataSources.remove(this._dataSource); this._dataSource = null; }
|
||||
this._count = 0;
|
||||
},
|
||||
|
||||
_fetch() {
|
||||
var self = this;
|
||||
var loadEl = document.getElementById('loading-disasters');
|
||||
var statusEl = document.getElementById('status-disasters');
|
||||
if (loadEl) loadEl.classList.add('active');
|
||||
if (statusEl) { statusEl.textContent = 'Lade Ereignisse...'; statusEl.classList.add('active'); }
|
||||
fetch('/api/disasters')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (!self._dataSource) return;
|
||||
self._dataSource.entities.removeAll();
|
||||
var events = data.events || [];
|
||||
self._count = events.length;
|
||||
var icons = {
|
||||
'wildfires': { color: '#ff4400', symbol: '🔥', label: 'Waldbrand' },
|
||||
'volcanoes': { color: '#ff0000', symbol: '🌋', label: 'Vulkan' },
|
||||
'severeStorms': { color: '#aa44ff', symbol: '🌀', label: 'Sturm' },
|
||||
'floods': { color: '#4488ff', symbol: '🌊', label: 'Flut' },
|
||||
'earthquakes': { color: '#ffaa00', symbol: '⚡', label: 'Erdbeben' },
|
||||
'seaLakeIce': { color: '#88ddff', symbol: '❄️', label: 'Eis' },
|
||||
'landslides': { color: '#886644', symbol: '⛰️', label: 'Erdrutsch' },
|
||||
};
|
||||
events.forEach(function(evt) {
|
||||
var cats = evt.categories || [];
|
||||
var catId = cats.length ? cats[0].id : 'unknown';
|
||||
var icon = icons[catId] || { color: '#ffffff', symbol: '⚠️', label: catId };
|
||||
var geom = evt.geometry || [];
|
||||
if (!geom.length) return;
|
||||
var latest = geom[geom.length - 1];
|
||||
var coords = latest.coordinates;
|
||||
if (!coords || coords.length < 2) return;
|
||||
var lon = coords[0], lat = coords[1];
|
||||
|
||||
self._dataSource.entities.add({
|
||||
position: Cesium.Cartesian3.fromDegrees(lon, lat, 0),
|
||||
point: {
|
||||
pixelSize: 8,
|
||||
color: Cesium.Color.fromCssColorString(icon.color),
|
||||
outlineColor: Cesium.Color.fromCssColorString(icon.color).withAlpha(0.4),
|
||||
outlineWidth: 3,
|
||||
heightReference: Cesium.HeightReference.NONE,
|
||||
},
|
||||
label: {
|
||||
text: icon.symbol,
|
||||
font: '14px sans-serif',
|
||||
pixelOffset: new Cesium.Cartesian2(0, -14),
|
||||
distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, 5000000),
|
||||
},
|
||||
description: '<div style="font-family:monospace;font-size:13px;padding:8px">' +
|
||||
'<strong style="color:' + icon.color + '">' + icon.label.toUpperCase() + '</strong><br>' +
|
||||
'<span style="color:#e0e0e0">' + (evt.title || '?') + '</span><br>' +
|
||||
'<span style="color:#888">Quellen: ' + (evt.sources || []).map(function(s) { return s.id; }).join(', ') + '</span>' +
|
||||
'</div>',
|
||||
});
|
||||
});
|
||||
if (statusEl) statusEl.textContent = events.length + ' Ereignisse';
|
||||
})
|
||||
.catch(function(e) { console.warn('Disasters error:', e); if (statusEl) statusEl.textContent = 'Fehler'; })
|
||||
.finally(function() { if (loadEl) loadEl.classList.remove('active'); setTimeout(function() { if (statusEl) statusEl.classList.remove('active'); }, 5000); });
|
||||
},
|
||||
};
|
||||
142
static/js/layers/satellites.js
Normale Datei
142
static/js/layers/satellites.js
Normale Datei
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Satelliten-Layer: Orbitale Positionen berechnet aus TLE-Daten.
|
||||
* Vereinfachte Kreisbahn-Berechnung (kein SGP4, reicht fuer Visualisierung).
|
||||
*/
|
||||
const SatellitesLayer = {
|
||||
_viewer: null,
|
||||
_points: null,
|
||||
_orbits: null,
|
||||
_interval: null,
|
||||
_count: 0,
|
||||
_data: [],
|
||||
_animFrame: null,
|
||||
|
||||
start(viewer) {
|
||||
if (this._points) return;
|
||||
this._viewer = viewer;
|
||||
this._points = viewer.scene.primitives.add(new Cesium.PointPrimitiveCollection());
|
||||
this._orbits = viewer.scene.primitives.add(new Cesium.PolylineCollection());
|
||||
this._fetch();
|
||||
var self = this;
|
||||
// Positionen alle 2s aktualisieren (Satelliten bewegen sich schnell)
|
||||
this._interval = setInterval(function() { self._updatePositions(); }, 2000);
|
||||
},
|
||||
|
||||
stop() {
|
||||
if (this._interval) { clearInterval(this._interval); this._interval = null; }
|
||||
if (this._points && this._viewer) { this._viewer.scene.primitives.remove(this._points); this._points = null; }
|
||||
if (this._orbits && this._viewer) { this._viewer.scene.primitives.remove(this._orbits); this._orbits = null; }
|
||||
this._count = 0; this._data = [];
|
||||
},
|
||||
|
||||
_fetch() {
|
||||
var self = this;
|
||||
var loadEl = document.getElementById('loading-satellites');
|
||||
var statusEl = document.getElementById('status-satellites');
|
||||
if (loadEl) loadEl.classList.add('active');
|
||||
if (statusEl) { statusEl.textContent = 'Lade Orbitdaten...'; statusEl.classList.add('active'); }
|
||||
fetch('/api/satellites')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
self._data = data.satellites || [];
|
||||
self._count = self._data.length;
|
||||
self._updatePositions();
|
||||
self._drawOrbits();
|
||||
if (statusEl) statusEl.textContent = self._count + ' Satelliten';
|
||||
})
|
||||
.catch(function(e) { console.warn('Satellites error:', e); if (statusEl) statusEl.textContent = 'Fehler'; })
|
||||
.finally(function() { if (loadEl) loadEl.classList.remove('active'); setTimeout(function() { if (statusEl) statusEl.classList.remove('active'); }, 5000); });
|
||||
},
|
||||
|
||||
_calcPosition(sat, time) {
|
||||
// Vereinfachte Kreisbahn aus Kepler-Elementen
|
||||
var n = sat.meanMotion; // Umlaeufe pro Tag
|
||||
if (!n || n <= 0) return null;
|
||||
var period = 86400 / n; // Umlaufzeit in Sekunden
|
||||
var a = Math.pow(398600.4418 * Math.pow(period / (2 * Math.PI), 2), 1/3); // Semi-major axis in km
|
||||
var altitude = a - 6371; // Hoehe ueber Erdoberflaeche in km
|
||||
if (altitude < 100 || altitude > 50000) return null;
|
||||
|
||||
var epochMs = new Date(sat.epoch).getTime();
|
||||
var elapsed = (time - epochMs) / 1000; // Sekunden seit Epoch
|
||||
var M = (sat.meanAnomaly || 0) + (360 * n / 86400) * elapsed; // Aktuelle Mean Anomaly
|
||||
M = ((M % 360) + 360) % 360; // Normalisieren
|
||||
|
||||
var inc = (sat.inclination || 0) * Math.PI / 180;
|
||||
var raan = (sat.raOfAscNode || 0) * Math.PI / 180;
|
||||
var argP = (sat.argOfPericenter || 0) * Math.PI / 180;
|
||||
var nu = M * Math.PI / 180; // True anomaly ≈ Mean anomaly fuer kleine Exzentrizitaet
|
||||
|
||||
// Position im Orbital-Frame
|
||||
var u = argP + nu;
|
||||
var r = a; // Kreisbahn-Naeherung
|
||||
|
||||
// ECI -> ECEF (vereinfacht: Erdrotation ignoriert fuer Visualisierung)
|
||||
var x = r * (Math.cos(raan) * Math.cos(u) - Math.sin(raan) * Math.sin(u) * Math.cos(inc));
|
||||
var y = r * (Math.sin(raan) * Math.cos(u) + Math.cos(raan) * Math.sin(u) * Math.cos(inc));
|
||||
var z = r * Math.sin(u) * Math.sin(inc);
|
||||
|
||||
// Erdrotation beruecksichtigen (grob)
|
||||
var gmst = elapsed * 7.2921159e-5; // Sternzeit-Winkel
|
||||
var xr = x * Math.cos(gmst) + y * Math.sin(gmst);
|
||||
var yr = -x * Math.sin(gmst) + y * Math.cos(gmst);
|
||||
|
||||
var lon = Math.atan2(yr, xr) * 180 / Math.PI;
|
||||
var lat = Math.asin(z / r) * 180 / Math.PI;
|
||||
|
||||
return { lat: lat, lon: lon, alt: altitude * 1000 }; // alt in Metern
|
||||
},
|
||||
|
||||
_updatePositions() {
|
||||
if (!this._points || !this._data.length) return;
|
||||
this._points.removeAll();
|
||||
var now = Date.now();
|
||||
var colors = {
|
||||
'stations': Cesium.Color.fromCssColorString('#ff4444'),
|
||||
'gps-ops': Cesium.Color.fromCssColorString('#ffaa00'),
|
||||
'galileo': Cesium.Color.fromCssColorString('#44aaff'),
|
||||
'weather': Cesium.Color.fromCssColorString('#aa44ff'),
|
||||
'resource': Cesium.Color.fromCssColorString('#44ffaa'),
|
||||
'starlink': Cesium.Color.fromCssColorString('#888888'),
|
||||
};
|
||||
for (var i = 0; i < this._data.length; i++) {
|
||||
var pos = this._calcPosition(this._data[i], now);
|
||||
if (!pos) continue;
|
||||
var color = colors[this._data[i].group] || Cesium.Color.WHITE;
|
||||
this._points.add({
|
||||
position: Cesium.Cartesian3.fromDegrees(pos.lon, pos.lat, pos.alt),
|
||||
pixelSize: this._data[i].group === 'starlink' ? 1.5 : 3,
|
||||
color: color,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_drawOrbits() {
|
||||
if (!this._orbits || !this._data.length) return;
|
||||
this._orbits.removeAll();
|
||||
var now = Date.now();
|
||||
var colors = {
|
||||
'stations': Cesium.Color.fromCssColorString('#ff4444').withAlpha(0.4),
|
||||
'gps-ops': Cesium.Color.fromCssColorString('#ffaa00').withAlpha(0.15),
|
||||
'galileo': Cesium.Color.fromCssColorString('#44aaff').withAlpha(0.15),
|
||||
};
|
||||
// Nur Orbits fuer Stationen, GPS, Galileo zeichnen (nicht Starlink — zu viele)
|
||||
for (var i = 0; i < this._data.length; i++) {
|
||||
var sat = this._data[i];
|
||||
if (!colors[sat.group]) continue;
|
||||
var positions = [];
|
||||
var period = 86400000 / (sat.meanMotion || 14); // Umlaufzeit in ms
|
||||
for (var t = 0; t <= period; t += period / 60) {
|
||||
var pos = this._calcPosition(sat, now + t);
|
||||
if (pos) positions.push(Cesium.Cartesian3.fromDegrees(pos.lon, pos.lat, pos.alt));
|
||||
}
|
||||
if (positions.length > 10) {
|
||||
this._orbits.add({
|
||||
positions: positions,
|
||||
width: sat.group === 'stations' ? 1.5 : 0.5,
|
||||
material: Cesium.Material.fromType('Color', { color: colors[sat.group] }),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
41
static/js/layers/visualmodes.js
Normale Datei
41
static/js/layers/visualmodes.js
Normale Datei
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Visual Modes: Nachtsicht (NVG), FLIR Thermal, CRT Scanlines.
|
||||
*/
|
||||
const VisualModes = {
|
||||
_current: 'standard',
|
||||
_overlay: null,
|
||||
|
||||
init() {
|
||||
this._overlay = document.getElementById('visual-overlay');
|
||||
},
|
||||
|
||||
set(mode) {
|
||||
this._current = mode;
|
||||
var container = document.getElementById('cesiumContainer');
|
||||
var overlay = this._overlay;
|
||||
|
||||
// Alle Mode-Klassen entfernen
|
||||
container.className = '';
|
||||
if (overlay) { overlay.className = 'visual-overlay'; overlay.style.display = 'none'; }
|
||||
|
||||
switch (mode) {
|
||||
case 'nvg':
|
||||
container.className = 'vmode-nvg';
|
||||
break;
|
||||
case 'flir':
|
||||
container.className = 'vmode-flir';
|
||||
break;
|
||||
case 'crt':
|
||||
container.className = 'vmode-crt';
|
||||
if (overlay) { overlay.className = 'visual-overlay vmode-crt-overlay'; overlay.style.display = 'block'; }
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Button-States aktualisieren
|
||||
document.querySelectorAll('.vmode-btn').forEach(function(btn) {
|
||||
btn.classList.toggle('active', btn.dataset.mode === mode);
|
||||
});
|
||||
},
|
||||
};
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren