Files
gods-eye-view/src/cockpitCloudEffects.js
T
Sameh Khamis 73e789489b chore: drop exports nothing imports
Knip, configured with the application, package exports, scripts, QA
harnesses, workers, tools and tests as entry points, reports exports
that no module imports. Each was checked by hand against string-based
and namespace imports.

- Symbols that are still used inside their own module lose only the
  `export` keyword (176 constants and helpers).
- Unused re-exports are removed from barrel modules (flight, military,
  vessel, CCTV, directions, transit history, local ADS-B and street-level
  policy modules).
- Declarations that nothing uses at all are deleted, among them
  AIS_WATCHDOG_STATUSES, applyFilter, isRadioCountryCode, compactText,
  DENSITY_STOPS, the unused CCTV endpoint constants, and the SDR band
  constants.
- Default exports that nothing imports are removed. The ones in
  src/data/earthquakes.js, src/data/telegeographySubmarineCables.js,
  src/data/wind.js and src/data/bhoteKoshiLocator.js each constructed a
  layer instance at import time that nothing used; those instances are
  no longer created.

Every export of a package.json entry stays, as does TOMTOM_CREDIT,
which the traffic layer reads from the credits namespace it is given.
2026-10-22 19:00:40 -07:00

602 lines
18 KiB
JavaScript

import { applicationServices } from './services/application.js';
import * as Cesium from 'cesium';
import { greatCircleMeters } from './geoDistance.js';
import {
deriveWeatherEffectProfile,
weatherAltitudeFactors,
} from './weatherEffectsMath.js';
const WEATHER_REFRESH_MS = 5 * 60_000;
const CLOUD_FRAME_MS = 1000 / 12;
const MAX_RENDER_WIDTH = 520;
const MAX_RENDER_HEIGHT = 320;
const WEATHER_MOVE_REFRESH_M = 25_000;
const WEATHER_ENABLED_STORAGE_KEY = 'godsEyeView.cockpitWeatherEffects.enabled';
const VERTEX_SHADER = `
attribute vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
`;
// Adapted from the supplied R&D cloudscape. This pass outputs transparent
// clouds only so Cesium remains the sky/terrain renderer. Three FBM octaves
// and 24 ray steps keep the presentation bounded on integrated GPUs.
const FRAGMENT_SHADER = `
precision highp float;
uniform vec2 uResolution;
uniform vec2 uWind;
uniform float uTime;
uniform float uStrength;
float hash(vec3 p) {
p = fract(p * vec3(443.8975, 397.2973, 491.1871));
p += dot(p, p.yxz + 19.19);
return fract((p.x + p.y) * p.z);
}
float noise(vec3 x) {
vec3 p = floor(x);
vec3 f = fract(x);
f = f * f * (3.0 - 2.0 * f);
return mix(
mix(
mix(hash(p), hash(p + vec3(1.0, 0.0, 0.0)), f.x),
mix(hash(p + vec3(0.0, 1.0, 0.0)), hash(p + vec3(1.0, 1.0, 0.0)), f.x),
f.y
),
mix(
mix(hash(p + vec3(0.0, 0.0, 1.0)), hash(p + vec3(1.0, 0.0, 1.0)), f.x),
mix(hash(p + vec3(0.0, 1.0, 1.0)), hash(p + vec3(1.0, 1.0, 1.0)), f.x),
f.y
),
f.z
);
}
float fbm(vec3 p) {
float value = 0.0;
float weight = 0.54;
for (int octave = 0; octave < 3; octave++) {
value += weight * noise(p);
p *= 2.03;
weight *= 0.48;
}
return value;
}
float cloudDensity(vec3 p) {
vec3 drift = vec3(uWind.x, 0.0, uWind.y) * uTime;
float warp = fbm((p + drift * 0.42) * 0.42);
vec3 warped = p + drift + vec3(warp * 1.25, 0.0, warp * 0.82);
float field = fbm(warped * 0.5);
float lower = smoothstep(0.45, 1.25, p.y);
float upper = 1.0 - smoothstep(3.8, 5.1, p.y);
float threshold = mix(0.71, 0.52, uStrength);
return smoothstep(threshold, 0.86, field) * lower * upper;
}
void main() {
vec2 uv = (gl_FragCoord.xy - 0.5 * uResolution.xy) / uResolution.y;
vec3 rayOrigin = vec3(0.0, 0.72, uTime * 0.035);
vec3 rayDirection = normalize(vec3(uv.x, uv.y * 0.92 + 0.18, 1.28));
vec3 sunDirection = normalize(vec3(0.45, 0.28, 0.84));
vec3 color = vec3(0.0);
float alpha = 0.0;
float distanceAlongRay = 0.15 + 0.06 * hash(rayDirection + uTime);
for (int stepIndex = 0; stepIndex < 24; stepIndex++) {
vec3 point = rayOrigin + rayDirection * distanceAlongRay;
float density = cloudDensity(point);
if (density > 0.012) {
float lightDensity = cloudDensity(point + sunDirection * 0.38);
float absorption = exp(-lightDensity * 3.8);
float powder = 1.0 - exp(-density * 2.2);
float lighting = mix(0.22, 1.0, absorption * powder);
vec3 cloudColor = mix(
vec3(0.31, 0.36, 0.43),
vec3(0.96, 0.96, 0.93),
lighting
);
float sampleAlpha = (1.0 - alpha) * density * (0.16 + uStrength * 0.13);
color += cloudColor * sampleAlpha;
alpha += sampleAlpha;
}
if (alpha > 0.92) break;
distanceAlongRay += 0.29;
}
float visorFade = smoothstep(-0.82, -0.28, uv.y);
alpha *= visorFade * uStrength * 0.78;
color = alpha > 0.001 ? color / max(alpha, 0.001) : vec3(0.0);
gl_FragColor = vec4(color, clamp(alpha, 0.0, 0.82));
}
`;
function compileShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) return shader;
const error =
gl.getShaderInfoLog(shader) || 'Unknown shader compilation error';
gl.deleteShader(shader);
throw new Error(error);
}
function greatCircleM(a, b) {
return greatCircleMeters(
{ lat: a?.latitude, lon: a?.longitude },
{ lat: b?.latitude, lon: b?.longitude },
);
}
/** Return whether cockpit weather must refresh for elapsed time or movement. */
export function cockpitWeatherRefreshDue({
nowMs,
fetchedAt,
anchor,
point,
hasWeather,
}) {
if (!hasWeather) return true;
if (!Number.isFinite(nowMs) || !Number.isFinite(fetchedAt)) return true;
return (
nowMs - fetchedAt >= WEATHER_REFRESH_MS ||
greatCircleM(anchor, point) >= WEATHER_MOVE_REFRESH_M
);
}
/** Returns the capped framebuffer size used by the cockpit cloud pass. */
export function cockpitCloudRenderSize(width, height) {
const viewportWidth = Math.max(1, Number(width) || 1);
const viewportHeight = Math.max(1, Number(height) || 1);
const scale = Math.min(
0.42,
MAX_RENDER_WIDTH / viewportWidth,
MAX_RENDER_HEIGHT / viewportHeight,
);
return {
width: Math.max(1, Math.round(viewportWidth * scale)),
height: Math.max(1, Math.round(viewportHeight * scale)),
};
}
/** Resolve the persisted cockpit-weather preference; missing values default off. */
export function cockpitWeatherEnabledFromStoredValue(value) {
return value === '1';
}
/**
* Weather-backed volumetric cloud pass that exists only while cockpit mode is
* active. It owns no Cesium fog/post-process stages and cannot affect map mode.
*/
class CockpitCloudEffectsController {
constructor(viewer, { weatherService = applicationServices.weather } = {}) {
this.weatherService = weatherService;
this.viewer = viewer;
this.canvas = document.createElement('canvas');
this.canvas.id = 'cockpit-cloud-effects';
this.canvas.setAttribute('aria-hidden', 'true');
document.body.appendChild(this.canvas);
this.gl = null;
this.program = null;
this.locations = null;
this.frame = null;
this.refreshTimer = null;
this.startupTimer = null;
this.lastFrameMs = 0;
this.lastRefreshCheckMs = 0;
this.fetchedAt = 0;
this.anchor = null;
this.weather = null;
this.strength = 0;
this.targetStrength = 0;
this.windDirectionDeg = 0;
this.windStrength = 0;
this.pending = null;
this.abort = null;
this.destroyed = false;
this.reducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)',
).matches;
this.enabled = this.readEnabledPreference();
this.onResize = () => this.resize();
this.onCockpitMode = (event) => {
const active =
event?.detail?.active ??
document.body.classList.contains('cockpit-mode');
if (active && this.enabled) this.start();
else this.stop();
};
this.onEnabledChange = (event) => {
const requested = event?.detail?.enabled;
this.setEnabled(
typeof requested === 'boolean' ? requested : !this.enabled,
);
};
window.addEventListener('resize', this.onResize);
window.addEventListener('gev:cockpit-mode-changed', this.onCockpitMode);
window.addEventListener('gev:cockpit-weather-toggle', this.onEnabledChange);
this.emitEnabledState();
}
readEnabledPreference() {
try {
return cockpitWeatherEnabledFromStoredValue(
localStorage.getItem(WEATHER_ENABLED_STORAGE_KEY),
);
} catch {
return false;
}
}
emitEnabledState() {
window.dispatchEvent(
new CustomEvent('gev:cockpit-weather-state', {
detail: { enabled: this.enabled },
}),
);
}
setEnabled(enabled) {
const next = !!enabled;
this.enabled = next;
try {
localStorage.setItem(WEATHER_ENABLED_STORAGE_KEY, next ? '1' : '0');
} catch {
/* best effort */
}
if (!next) {
this.stop();
} else if (document.body.classList.contains('cockpit-mode')) {
this.start();
}
this.emitEnabledState();
}
initializeRenderer() {
try {
const gl = this.canvas.getContext('webgl', {
alpha: true,
antialias: false,
depth: false,
stencil: false,
premultipliedAlpha: false,
preserveDrawingBuffer: false,
powerPreference: 'low-power',
});
if (!gl) throw new Error('WebGL unavailable');
const vertexShader = compileShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER);
const fragmentShader = compileShader(
gl,
gl.FRAGMENT_SHADER,
FRAGMENT_SHADER,
);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw new Error(
gl.getProgramInfoLog(program) || 'Cloud shader link failed',
);
}
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
gl.STATIC_DRAW,
);
const position = gl.getAttribLocation(program, 'position');
gl.enableVertexAttribArray(position);
gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
this.gl = gl;
this.program = program;
this.locations = {
resolution: gl.getUniformLocation(program, 'uResolution'),
time: gl.getUniformLocation(program, 'uTime'),
strength: gl.getUniformLocation(program, 'uStrength'),
wind: gl.getUniformLocation(program, 'uWind'),
};
} catch (error) {
console.warn('[Cockpit clouds] Renderer unavailable:', error);
this.canvas.dataset.status = 'unavailable';
}
}
resize() {
if (!this.gl) return;
const size = cockpitCloudRenderSize(window.innerWidth, window.innerHeight);
if (this.canvas.width === size.width && this.canvas.height === size.height)
return;
this.canvas.width = size.width;
this.canvas.height = size.height;
this.gl.viewport(0, 0, size.width, size.height);
}
cameraPoint() {
const cartographic = this.viewer?.camera?.positionCartographic;
if (!cartographic) return null;
const latitude = Cesium.Math.toDegrees(cartographic.latitude);
const longitude = Cesium.Math.toDegrees(cartographic.longitude);
if (![latitude, longitude].every(Number.isFinite)) return null;
return {
latitude,
longitude,
altitudeM: Math.max(0, Number(cartographic.height) || 0),
};
}
start() {
if (
this.destroyed ||
!this.enabled ||
this.frame !== null ||
this.suspended
)
return;
if (!this.gl) {
this.initializeRenderer();
this.resize();
}
if (!this.gl) return;
this.canvas.dataset.cockpit = 'true';
this.lastFrameMs = 0;
this.frame = requestAnimationFrame((time) => this.tick(time));
if (this.startupTimer !== null) window.clearTimeout(this.startupTimer);
this.startupTimer = window.setTimeout(() => {
this.startupTimer = null;
if (document.body.classList.contains('cockpit-mode')) void this.refresh();
}, 450);
}
stop() {
if (this.frame !== null) cancelAnimationFrame(this.frame);
this.frame = null;
if (this.refreshTimer !== null) window.clearTimeout(this.refreshTimer);
this.refreshTimer = null;
if (this.startupTimer !== null) window.clearTimeout(this.startupTimer);
this.startupTimer = null;
this.abort?.abort();
this.abort = null;
this.pending = null;
this.strength = 0;
this.targetStrength = 0;
this.canvas.classList.remove('active');
this.canvas.dataset.cockpit = 'false';
this.clear();
}
clear() {
if (!this.gl) return;
this.gl.clearColor(0, 0, 0, 0);
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
}
async refresh() {
if (
this.destroyed ||
!this.enabled ||
this.pending ||
this.suspended ||
!document.body.classList.contains('cockpit-mode')
) {
return this.pending;
}
const point = this.cameraPoint();
if (!point) return null;
if (
this.weather &&
Date.now() - this.fetchedAt < WEATHER_REFRESH_MS &&
greatCircleM(this.anchor, point) < WEATHER_MOVE_REFRESH_M
) {
this.applyWeather(this.weather, point.altitudeM);
return this.weather;
}
this.abort?.abort();
this.abort = new AbortController();
const pending = this.weatherService.getConditions(
point.latitude,
point.longitude,
{ signal: this.abort.signal },
);
const request = this.abort;
this.pending = pending
.then(async (payload) => {
if (this.abort !== request || request.signal.aborted) return null;
if (!payload?.weather)
throw new Error('Cloud weather observation unavailable');
this.weather = payload.weather;
this.fetchedAt = Date.now();
this.anchor = point;
this.applyWeather(payload.weather, point.altitudeM);
this.canvas.dataset.sourceStatus = payload.status || 'ready';
return payload.weather;
})
.catch((error) => {
if (request.signal.aborted || this.abort !== request) return;
if (error?.name !== 'AbortError') {
this.targetStrength = 0;
this.canvas.dataset.sourceStatus = 'unavailable';
}
return null;
})
.finally(() => {
if (this.abort === request) {
this.pending = null;
this.abort = null;
}
});
return this.pending;
}
applyWeather(weather, altitudeM) {
const profile = deriveWeatherEffectProfile(weather);
const altitude = weatherAltitudeFactors(altitudeM);
this.targetStrength = profile.available
? profile.cloud * altitude.cloud
: 0;
this.windDirectionDeg = profile.windDirectionDeg || 0;
this.windStrength = profile.wind || 0;
this.canvas.dataset.cloudStrength = this.targetStrength.toFixed(3);
this.canvas.dataset.weatherCode = String(weather?.weatherCode ?? 'unknown');
if (
this.reducedMotion &&
document.body.classList.contains('cockpit-mode')
) {
this.strength = this.targetStrength;
const visible = this.strength > 0.035;
this.canvas.classList.toggle('active', visible);
if (visible) this.render(0);
else this.clear();
}
}
tick(timeMs) {
if (
this.destroyed ||
!this.enabled ||
!document.body.classList.contains('cockpit-mode')
) {
this.stop();
return;
}
// Suspended (document hidden): halt every scheduling chain — rAF AND the
// reduced-motion timeout chain — without tearing weather state down.
// setSuspended(false) restarts exactly one chain. (perf wave 2 fix)
if (this.suspended) {
this.frame = null;
if (this.refreshTimer !== null) window.clearTimeout(this.refreshTimer);
this.refreshTimer = null;
return;
}
if (timeMs - this.lastRefreshCheckMs >= 1000) {
this.lastRefreshCheckMs = timeMs;
const point = this.cameraPoint();
if (
point &&
cockpitWeatherRefreshDue({
nowMs: Date.now(),
fetchedAt: this.fetchedAt,
anchor: this.anchor,
point,
hasWeather: Boolean(this.weather),
})
)
void this.refresh();
}
const blend = this.reducedMotion ? 1 : 0.08;
this.strength += (this.targetStrength - this.strength) * blend;
const visible = this.strength > 0.035;
this.canvas.classList.toggle('active', visible);
if (
visible &&
(this.reducedMotion || timeMs - this.lastFrameMs >= CLOUD_FRAME_MS)
) {
this.lastFrameMs = timeMs;
this.render(timeMs / 1000);
} else if (!visible) {
this.clear();
}
if (this.reducedMotion) {
this.frame = null;
this.refreshTimer = window.setTimeout(() => {
this.refreshTimer = null;
this.tick(performance.now());
}, 1000);
return;
}
this.frame = requestAnimationFrame((time) => this.tick(time));
}
render(timeSec) {
const gl = this.gl;
if (!gl || !this.program || !this.locations) return;
const windRadians = Cesium.Math.toRadians(this.windDirectionDeg);
const windScale = 0.008 + this.windStrength * 0.022;
gl.useProgram(this.program);
gl.uniform2f(
this.locations.resolution,
this.canvas.width,
this.canvas.height,
);
gl.uniform1f(this.locations.time, timeSec);
gl.uniform1f(
this.locations.strength,
Math.min(1, Math.max(0, this.strength)),
);
gl.uniform2f(
this.locations.wind,
Math.sin(windRadians) * windScale,
-Math.cos(windRadians) * windScale,
);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
getSnapshot() {
return {
enabled: this.enabled,
active: this.canvas.classList.contains('active'),
strength: this.targetStrength,
weather: this.weather ? { ...this.weather } : null,
renderSize: { width: this.canvas.width, height: this.canvas.height },
sourceStatus: this.canvas.dataset.sourceStatus || 'idle',
};
}
/**
* Hidden-state gate (perf wave 2): pause the independent cloud rAF while
* the document is hidden; resume it on return if cockpit mode is still up.
* @param {boolean} suspended
* @returns {void}
*/
setSuspended(suspended) {
this.suspended = Boolean(suspended);
if (this.suspended) {
if (this.frame !== null) cancelAnimationFrame(this.frame);
this.frame = null;
// The reduced-motion path schedules via timeout, not rAF — clear it
// too, plus the startup refresh timer; refresh() is suspend-guarded.
if (this.refreshTimer !== null) window.clearTimeout(this.refreshTimer);
this.refreshTimer = null;
if (this.startupTimer !== null) window.clearTimeout(this.startupTimer);
this.startupTimer = null;
} else if (document.body.classList.contains('cockpit-mode')) {
this.start();
}
}
destroy() {
this.destroyed = true;
this.stop();
window.removeEventListener('resize', this.onResize);
window.removeEventListener('gev:cockpit-mode-changed', this.onCockpitMode);
window.removeEventListener(
'gev:cockpit-weather-toggle',
this.onEnabledChange,
);
if (this.program && this.gl) this.gl.deleteProgram(this.program);
this.canvas.remove();
}
}
export function initCockpitCloudEffects(viewer, options) {
return new CockpitCloudEffectsController(viewer, options);
}