mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
refactor(app): share application components and request services
This commit is contained in:
1 parent
a0a0128a59
commit
a67bc60a29
84 files changed
+2798
-1731
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { createSourceSlot } from '../app/sourceSlot.js';
|
||||
import {
|
||||
createAlprCamerasLayer,
|
||||
createOverpassAlprSource,
|
||||
@@ -7,7 +8,13 @@ import * as context from './contextStore.js';
|
||||
import * as picking from './pickRegistry.js';
|
||||
import * as groundFloor from './groundFloor.js';
|
||||
export * from '../layers/alpr/index.js';
|
||||
const slot = createSourceSlot(
|
||||
createOverpassAlprSource(),
|
||||
['fetch'],
|
||||
'ALPR source',
|
||||
);
|
||||
export const configureAlprSource = slot.configure;
|
||||
export default createAlprCamerasLayer({
|
||||
source: createOverpassAlprSource(),
|
||||
source: slot.source,
|
||||
services: { render, context, picking, groundFloor },
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createSourceSlot } from '../app/sourceSlot.js';
|
||||
import { createBikeshareSource } from '../layers/bikeshare/source.js';
|
||||
import { createBikeshareLayer } from '../layers/bikeshare/index.js';
|
||||
import * as render from '../renderGovernor.js';
|
||||
@@ -5,8 +6,14 @@ import * as sprites from './spriteOrder.js';
|
||||
import * as picking from './pickRegistry.js';
|
||||
import * as overlays from '../overlays/worldOverlay.js';
|
||||
|
||||
const sourceSlot = createSourceSlot(
|
||||
createBikeshareSource(),
|
||||
['getStations'],
|
||||
'Bikeshare source',
|
||||
);
|
||||
export const configureBikeshareSource = sourceSlot.configure;
|
||||
const layer = createBikeshareLayer({
|
||||
source: createBikeshareSource(),
|
||||
source: sourceSlot.source,
|
||||
services: { render, sprites, picking, overlays },
|
||||
});
|
||||
export const createBikeshareSelectedOverlayEntry =
|
||||
|
||||
+8
-1
@@ -1,3 +1,4 @@
|
||||
import { createSourceSlot } from '../app/sourceSlot.js';
|
||||
import { createCctvSource } from '../layers/cctv/source.js';
|
||||
import { createCctvLayer } from '../layers/cctv/index.js';
|
||||
import * as sprites from './spriteOrder.js';
|
||||
@@ -11,8 +12,14 @@ import * as mesh from './meshFloorSampler.js';
|
||||
import * as focus from './focusDeemphasis.js';
|
||||
import * as render from '../renderGovernor.js';
|
||||
|
||||
const sourceSlot = createSourceSlot(
|
||||
createCctvSource(),
|
||||
['getCatalog', 'getHealth', 'getFrameUrl', 'getMediaUrl'],
|
||||
'Cctv source',
|
||||
);
|
||||
export const configureCctvSource = sourceSlot.configure;
|
||||
const layer = createCctvLayer({
|
||||
source: createCctvSource(),
|
||||
source: sourceSlot.source,
|
||||
services: {
|
||||
sprites,
|
||||
activation,
|
||||
|
||||
@@ -300,12 +300,12 @@ export function registerDynamicCredit(viewer, credit) {
|
||||
* present in the "Data attribution" popover.
|
||||
* @param {Cesium.Viewer} viewer — the initialized Cesium viewer
|
||||
*/
|
||||
export function registerDataCredits(viewer) {
|
||||
export function registerDataCredits(viewer, credits = DATA_CREDITS) {
|
||||
const creditDisplay = viewer?.creditDisplay;
|
||||
if (!creditDisplay || typeof creditDisplay.addStaticCredit !== 'function') {
|
||||
return;
|
||||
}
|
||||
for (const { html } of DATA_CREDITS) {
|
||||
for (const { html } of credits) {
|
||||
// showOnScreen=false → lives in the expandable "Data attribution" popover,
|
||||
// not the on-globe credit line.
|
||||
creditDisplay.addStaticCredit(new Cesium.Credit(html, false));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { expandApplicationHtml } from '../../build/application-html.js';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
@@ -18,7 +19,7 @@ import {
|
||||
} from './detectionPolicy.js';
|
||||
import { KEYHOLE_OUTSIDE_OPACITY_DEFAULT } from '../celestialRing.js';
|
||||
|
||||
const indexHtml = fs.readFileSync(new URL('../../index.html', import.meta.url), 'utf8');
|
||||
const indexHtml = expandApplicationHtml(fs.readFileSync(new URL('../../index.html', import.meta.url), 'utf8'));
|
||||
|
||||
test('side aircraft brackets stay readable without changing zero-opacity intent', () => {
|
||||
assert.equal(detectionBracketAlpha('AIR', 0), 0);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createSourceSlot } from '../app/sourceSlot.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import {
|
||||
createFirmsHeatmapLayer as createLayer,
|
||||
@@ -20,6 +21,12 @@ const services = {
|
||||
overlays,
|
||||
focus,
|
||||
};
|
||||
const sourceSlot = createSourceSlot(
|
||||
createFirmsSource(),
|
||||
['getSnapshot'],
|
||||
'Fire source',
|
||||
);
|
||||
export const configureFirmsSource = sourceSlot.configure;
|
||||
const helpers = createFirmsHelpers({ services });
|
||||
export const mapAnalystRecord = helpers.mapAnalystRecord;
|
||||
export const fireCullPosition = helpers.fireCullPosition;
|
||||
@@ -35,7 +42,7 @@ export function createFirmsHeatmapLayer(options) {
|
||||
...options,
|
||||
icon: options.icon ?? '▲',
|
||||
source: options.source ?? 'NASA FIRMS',
|
||||
feed: options.feed ?? createFirmsSource(),
|
||||
feed: options.feed ?? sourceSlot.source,
|
||||
services,
|
||||
overlayHost: options.overlayHost ?? {
|
||||
setEntries: overlays.setOverlayEntries,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { expandApplicationHtml } from '../../build/application-html.js';
|
||||
import { readLayerSource } from '../testSupport/readLayerSource.mjs';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
@@ -377,7 +378,7 @@ test('a fresh boot starts 3D aircraft ON in proximity — codec, both layers, an
|
||||
'ui.js: the DISPLAY rail believes 3D is on before any layer-state sync arrives');
|
||||
assert.match(ui, /this\.(?:flightState\.)?_models3dMode = 'proximity';/,
|
||||
'ui.js: and believes the mode is proximity');
|
||||
const html = await readFile(new URL('../../index.html', import.meta.url), 'utf8');
|
||||
const html = expandApplicationHtml(await readFile(new URL('../../index.html', import.meta.url), 'utf8'));
|
||||
assert.match(html, /class="pp-toggle-btn active" id="models3d-toggle" aria-pressed="true"/,
|
||||
'index.html: the 3D button paints lit on first paint, before ui.js runs — and says so');
|
||||
assert.match(ui, /this\._models3dBtn\?\.setAttribute\(\s*'aria-pressed',\s*String\(this\.(?:flightState\.)?_models3dEnabled\),?\s*\)/,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createSourceSlot } from '../app/sourceSlot.js';
|
||||
import {
|
||||
createInstallationsLayer,
|
||||
createInstallationSource,
|
||||
@@ -8,8 +9,14 @@ import * as ground from './groundFloor.js';
|
||||
import * as anchors from './fireAnchors.js';
|
||||
import * as picking from './pickRegistry.js';
|
||||
|
||||
const sourceSlot = createSourceSlot(
|
||||
createInstallationSource(),
|
||||
['getMappedSites', 'searchNearby'],
|
||||
'Installation source',
|
||||
);
|
||||
export const configureInstallationSource = sourceSlot.configure;
|
||||
const layer = createInstallationsLayer({
|
||||
source: createInstallationSource(),
|
||||
source: sourceSlot.source,
|
||||
services: { render, context, ground, anchors, picking },
|
||||
});
|
||||
export const approximateSurfaceDistanceM = layer.approximateSurfaceDistanceM;
|
||||
|
||||
@@ -9,3 +9,6 @@ export const registerMilitaryIcaos = registry.registerMilitaryIcaos;
|
||||
export const isMilitaryIcao = registry.isMilitaryIcao;
|
||||
export const refreshMilitaryRegistryIfStale =
|
||||
registry.refreshMilitaryRegistryIfStale;
|
||||
|
||||
/** Configure classification from the same source used by the aircraft layer. */
|
||||
export const configureMilitaryRegistrySource = registry.configureSource;
|
||||
+8
-1
@@ -1,3 +1,4 @@
|
||||
import { createSourceSlot } from '../app/sourceSlot.js';
|
||||
import { createRadioSource } from '../layers/radio/source.js';
|
||||
import { createRadioLayer } from '../layers/radio/index.js';
|
||||
import * as ground from './groundFloor.js';
|
||||
@@ -6,8 +7,14 @@ import * as overlays from '../overlays/worldOverlay.js';
|
||||
import * as globe from '../celestialRing.js';
|
||||
import * as render from '../renderGovernor.js';
|
||||
|
||||
const sourceSlot = createSourceSlot(
|
||||
createRadioSource(),
|
||||
['getDirectory', 'recordClick'],
|
||||
'Radio source',
|
||||
);
|
||||
export const configureRadioSource = sourceSlot.configure;
|
||||
const layer = createRadioLayer({
|
||||
source: createRadioSource(),
|
||||
source: sourceSlot.source,
|
||||
services: { ground, picking, overlays, globe, render },
|
||||
});
|
||||
export const radioGlobeLabel = layer.radioGlobeLabel;
|
||||
|
||||
+3
-127
@@ -1,131 +1,7 @@
|
||||
const MAX_ARTICLES = 5;
|
||||
|
||||
function cleanText(value, maxLength = 180) {
|
||||
return String(value || '').replace(/\s+/g, ' ').trim().slice(0, maxLength);
|
||||
}
|
||||
|
||||
function safeHttpUrl(value) {
|
||||
try {
|
||||
const parsed = new URL(String(value || ''));
|
||||
return parsed.protocol === 'http:' || parsed.protocol === 'https:' ? parsed.href : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Normalize a Nominatim reverse-geocode response into cockpit-sized place context. */
|
||||
export function normalizeRegionalPlace(payload) {
|
||||
const address = payload?.address || {};
|
||||
const locality = cleanText(
|
||||
address.city || address.town || address.village || address.municipality
|
||||
|| address.hamlet || address.county,
|
||||
90,
|
||||
);
|
||||
const region = cleanText(address.state || address.region || address.county, 90);
|
||||
const country = cleanText(address.country, 90);
|
||||
const label = [locality, region].filter((value, index, values) => value && values.indexOf(value) === index)
|
||||
.join(', ') || country || cleanText(payload?.display_name, 120);
|
||||
if (!label) return null;
|
||||
return {
|
||||
label,
|
||||
locality: locality || null,
|
||||
region: region || null,
|
||||
country: country || null,
|
||||
countryCode: cleanText(address.country_code, 4).toUpperCase() || null,
|
||||
};
|
||||
}
|
||||
|
||||
/** Normalize and deduplicate GDELT ArticleList output without trusting article HTML. */
|
||||
export function normalizeRegionalArticles(payload, limit = MAX_ARTICLES) {
|
||||
const rows = Array.isArray(payload?.articles) ? payload.articles : [];
|
||||
const seen = new Set();
|
||||
const articles = [];
|
||||
for (const row of rows) {
|
||||
const url = safeHttpUrl(row?.url || row?.url_mobile);
|
||||
const title = cleanText(row?.title, 180);
|
||||
if (!url || !title) continue;
|
||||
const signature = `${title.toLowerCase()}|${new URL(url).hostname}`;
|
||||
if (seen.has(signature)) continue;
|
||||
seen.add(signature);
|
||||
const rawDate = cleanText(row?.seendate, 32);
|
||||
const compactDate = /^(\d{8})T(\d{6})Z$/.exec(rawDate);
|
||||
const publishedAt = compactDate
|
||||
? `${compactDate[1].slice(0, 4)}-${compactDate[1].slice(4, 6)}-${compactDate[1].slice(6, 8)}T${compactDate[2].slice(0, 2)}:${compactDate[2].slice(2, 4)}:${compactDate[2].slice(4, 6)}Z`
|
||||
: Number.isNaN(Date.parse(rawDate)) ? null : new Date(rawDate).toISOString();
|
||||
articles.push({
|
||||
title,
|
||||
url,
|
||||
domain: cleanText(row?.domain || new URL(url).hostname.replace(/^www\./, ''), 80),
|
||||
publishedAt,
|
||||
sourceCountry: cleanText(row?.sourcecountry, 60) || null,
|
||||
});
|
||||
if (articles.length >= Math.max(1, Math.min(MAX_ARTICLES, limit))) break;
|
||||
}
|
||||
return articles;
|
||||
}
|
||||
|
||||
/** Normalize Open-Meteo current conditions into a small source-stamped record. */
|
||||
export function normalizeRegionalWeather(payload) {
|
||||
const current = payload?.current;
|
||||
if (!current || !Number.isFinite(Number(current.temperature_2m))) return null;
|
||||
const numberOrNull = (value) => {
|
||||
if (value === null || value === undefined || value === '') return null;
|
||||
return Number.isFinite(Number(value)) ? Number(value) : null;
|
||||
};
|
||||
// Open-Meteo reports zone-naive timestamps ("2026-08-17T00:15") that are UTC,
|
||||
// but JS parses zoneless date-times as LOCAL — pin them to UTC explicitly.
|
||||
const observedRaw = typeof current.time === 'string' && !/(?:[zZ]|[+-]\d\d:?\d\d)$/.test(current.time)
|
||||
? `${current.time}Z`
|
||||
: current.time;
|
||||
return {
|
||||
observedAt: Number.isNaN(Date.parse(observedRaw)) ? null : new Date(observedRaw).toISOString(),
|
||||
temperatureC: numberOrNull(current.temperature_2m),
|
||||
apparentTemperatureC: numberOrNull(current.apparent_temperature),
|
||||
precipitationMm: numberOrNull(current.precipitation),
|
||||
cloudCoverPct: numberOrNull(current.cloud_cover),
|
||||
windKph: numberOrNull(current.wind_speed_10m),
|
||||
windDirectionDeg: numberOrNull(current.wind_direction_10m),
|
||||
visibilityM: numberOrNull(current.visibility),
|
||||
weatherCode: numberOrNull(current.weather_code),
|
||||
};
|
||||
}
|
||||
|
||||
/** Translate the WMO weather code used by Open-Meteo into concise cockpit copy. */
|
||||
export function weatherCodeLabel(code) {
|
||||
const value = Number(code);
|
||||
if (!Number.isFinite(value)) return 'CONDITIONS UNKNOWN';
|
||||
if (value === 0) return 'CLEAR';
|
||||
if ([1, 2].includes(value)) return 'PARTLY CLOUDY';
|
||||
if (value === 3) return 'OVERCAST';
|
||||
if ([45, 48].includes(value)) return 'FOG';
|
||||
if (value >= 51 && value <= 57) return 'DRIZZLE';
|
||||
if (value >= 61 && value <= 67) return 'RAIN';
|
||||
if (value >= 71 && value <= 77) return 'SNOW';
|
||||
if (value >= 80 && value <= 82) return 'RAIN SHOWERS';
|
||||
if (value >= 85 && value <= 86) return 'SNOW SHOWERS';
|
||||
if (value >= 95) return 'THUNDERSTORM';
|
||||
return 'MIXED CONDITIONS';
|
||||
}
|
||||
|
||||
/** Great-circle distance used to avoid refetching a regional brief every animation frame. */
|
||||
export function regionalDistanceM(from, to) {
|
||||
if (![from?.latitude, from?.longitude, to?.latitude, to?.longitude].every(Number.isFinite)) {
|
||||
return Infinity;
|
||||
}
|
||||
const phi1 = from.latitude * Math.PI / 180;
|
||||
const phi2 = to.latitude * Math.PI / 180;
|
||||
const deltaPhi = (to.latitude - from.latitude) * Math.PI / 180;
|
||||
const deltaLambda = (to.longitude - from.longitude) * Math.PI / 180;
|
||||
const a = Math.sin(deltaPhi / 2) ** 2
|
||||
+ Math.cos(phi1) * Math.cos(phi2) * Math.sin(deltaLambda / 2) ** 2;
|
||||
return 6371000 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
}
|
||||
|
||||
import { applicationServices } from '../services/application.js';
|
||||
export * from './regionalModel.js';
|
||||
/** Fetch a bounded regional brief through the same-origin dev/preview proxy. */
|
||||
export async function fetchRegionalBrief(latitude, longitude, { signal } = {}) {
|
||||
if (![latitude, longitude].every(Number.isFinite)) throw new Error('Valid coordinates are required');
|
||||
const params = new URLSearchParams({ latitude: latitude.toFixed(5), longitude: longitude.toFixed(5) });
|
||||
const response = await fetch(`/api/regional-brief?${params}`, { signal });
|
||||
if (!response.ok) throw new Error(`Regional brief unavailable (${response.status})`);
|
||||
return response.json();
|
||||
return applicationServices.regional.getBrief(latitude, longitude, { signal });
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
const MAX_ARTICLES = 5;
|
||||
|
||||
function cleanText(value, maxLength = 180) {
|
||||
return String(value || '').replace(/\s+/g, ' ').trim().slice(0, maxLength);
|
||||
}
|
||||
|
||||
function safeHttpUrl(value) {
|
||||
try {
|
||||
const parsed = new URL(String(value || ''));
|
||||
return parsed.protocol === 'http:' || parsed.protocol === 'https:' ? parsed.href : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Normalize a Nominatim reverse-geocode response into cockpit-sized place context. */
|
||||
export function normalizeRegionalPlace(payload) {
|
||||
const address = payload?.address || {};
|
||||
const locality = cleanText(
|
||||
address.city || address.town || address.village || address.municipality
|
||||
|| address.hamlet || address.county,
|
||||
90,
|
||||
);
|
||||
const region = cleanText(address.state || address.region || address.county, 90);
|
||||
const country = cleanText(address.country, 90);
|
||||
const label = [locality, region].filter((value, index, values) => value && values.indexOf(value) === index)
|
||||
.join(', ') || country || cleanText(payload?.display_name, 120);
|
||||
if (!label) return null;
|
||||
return {
|
||||
label,
|
||||
locality: locality || null,
|
||||
region: region || null,
|
||||
country: country || null,
|
||||
countryCode: cleanText(address.country_code, 4).toUpperCase() || null,
|
||||
};
|
||||
}
|
||||
|
||||
/** Normalize and deduplicate GDELT ArticleList output without trusting article HTML. */
|
||||
export function normalizeRegionalArticles(payload, limit = MAX_ARTICLES) {
|
||||
const rows = Array.isArray(payload?.articles) ? payload.articles : [];
|
||||
const seen = new Set();
|
||||
const articles = [];
|
||||
for (const row of rows) {
|
||||
const url = safeHttpUrl(row?.url || row?.url_mobile);
|
||||
const title = cleanText(row?.title, 180);
|
||||
if (!url || !title) continue;
|
||||
const signature = `${title.toLowerCase()}|${new URL(url).hostname}`;
|
||||
if (seen.has(signature)) continue;
|
||||
seen.add(signature);
|
||||
const rawDate = cleanText(row?.seendate, 32);
|
||||
const compactDate = /^(\d{8})T(\d{6})Z$/.exec(rawDate);
|
||||
const publishedAt = compactDate
|
||||
? `${compactDate[1].slice(0, 4)}-${compactDate[1].slice(4, 6)}-${compactDate[1].slice(6, 8)}T${compactDate[2].slice(0, 2)}:${compactDate[2].slice(2, 4)}:${compactDate[2].slice(4, 6)}Z`
|
||||
: Number.isNaN(Date.parse(rawDate)) ? null : new Date(rawDate).toISOString();
|
||||
articles.push({
|
||||
title,
|
||||
url,
|
||||
domain: cleanText(row?.domain || new URL(url).hostname.replace(/^www\./, ''), 80),
|
||||
publishedAt,
|
||||
sourceCountry: cleanText(row?.sourcecountry, 60) || null,
|
||||
});
|
||||
if (articles.length >= Math.max(1, Math.min(MAX_ARTICLES, limit))) break;
|
||||
}
|
||||
return articles;
|
||||
}
|
||||
|
||||
/** Normalize Open-Meteo current conditions into a small source-stamped record. */
|
||||
export function normalizeRegionalWeather(payload) {
|
||||
const current = payload?.current;
|
||||
if (!current || !Number.isFinite(Number(current.temperature_2m))) return null;
|
||||
const numberOrNull = (value) => {
|
||||
if (value === null || value === undefined || value === '') return null;
|
||||
return Number.isFinite(Number(value)) ? Number(value) : null;
|
||||
};
|
||||
// Open-Meteo reports zone-naive timestamps ("2026-08-17T00:15") that are UTC,
|
||||
// but JS parses zoneless date-times as LOCAL — pin them to UTC explicitly.
|
||||
const observedRaw = typeof current.time === 'string' && !/(?:[zZ]|[+-]\d\d:?\d\d)$/.test(current.time)
|
||||
? `${current.time}Z`
|
||||
: current.time;
|
||||
return {
|
||||
observedAt: Number.isNaN(Date.parse(observedRaw)) ? null : new Date(observedRaw).toISOString(),
|
||||
temperatureC: numberOrNull(current.temperature_2m),
|
||||
apparentTemperatureC: numberOrNull(current.apparent_temperature),
|
||||
precipitationMm: numberOrNull(current.precipitation),
|
||||
cloudCoverPct: numberOrNull(current.cloud_cover),
|
||||
windKph: numberOrNull(current.wind_speed_10m),
|
||||
windDirectionDeg: numberOrNull(current.wind_direction_10m),
|
||||
visibilityM: numberOrNull(current.visibility),
|
||||
weatherCode: numberOrNull(current.weather_code),
|
||||
};
|
||||
}
|
||||
|
||||
/** Translate the WMO weather code used by Open-Meteo into concise cockpit copy. */
|
||||
export function weatherCodeLabel(code) {
|
||||
const value = Number(code);
|
||||
if (!Number.isFinite(value)) return 'CONDITIONS UNKNOWN';
|
||||
if (value === 0) return 'CLEAR';
|
||||
if ([1, 2].includes(value)) return 'PARTLY CLOUDY';
|
||||
if (value === 3) return 'OVERCAST';
|
||||
if ([45, 48].includes(value)) return 'FOG';
|
||||
if (value >= 51 && value <= 57) return 'DRIZZLE';
|
||||
if (value >= 61 && value <= 67) return 'RAIN';
|
||||
if (value >= 71 && value <= 77) return 'SNOW';
|
||||
if (value >= 80 && value <= 82) return 'RAIN SHOWERS';
|
||||
if (value >= 85 && value <= 86) return 'SNOW SHOWERS';
|
||||
if (value >= 95) return 'THUNDERSTORM';
|
||||
return 'MIXED CONDITIONS';
|
||||
}
|
||||
|
||||
/** Great-circle distance used to avoid refetching a regional brief every animation frame. */
|
||||
export function regionalDistanceM(from, to) {
|
||||
if (![from?.latitude, from?.longitude, to?.latitude, to?.longitude].every(Number.isFinite)) {
|
||||
return Infinity;
|
||||
}
|
||||
const phi1 = from.latitude * Math.PI / 180;
|
||||
const phi2 = to.latitude * Math.PI / 180;
|
||||
const deltaPhi = (to.latitude - from.latitude) * Math.PI / 180;
|
||||
const deltaLambda = (to.longitude - from.longitude) * Math.PI / 180;
|
||||
const a = Math.sin(deltaPhi / 2) ** 2
|
||||
+ Math.cos(phi1) * Math.cos(phi2) * Math.sin(deltaLambda / 2) ** 2;
|
||||
return 6371000 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createSourceSlot } from '../app/sourceSlot.js';
|
||||
import {
|
||||
createRocketLaunchesLayer,
|
||||
createLaunchSource,
|
||||
@@ -7,8 +8,14 @@ import * as geometry from '../celestialRing.js';
|
||||
import * as overlays from '../overlays/worldOverlay.js';
|
||||
import * as render from '../renderGovernor.js';
|
||||
|
||||
const sourceSlot = createSourceSlot(
|
||||
createLaunchSource(),
|
||||
['getLaunches', 'getActiveTle'],
|
||||
'Launch source',
|
||||
);
|
||||
export const configureLaunchSource = sourceSlot.configure;
|
||||
const layer = createRocketLaunchesLayer({
|
||||
source: createLaunchSource(),
|
||||
source: sourceSlot.source,
|
||||
services: { satellites, geometry, overlays, render },
|
||||
});
|
||||
export const satelliteParamsForSpaceMissions =
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createSourceSlot } from '../app/sourceSlot.js';
|
||||
import {
|
||||
createSatellitesLayer,
|
||||
createSatelliteSource,
|
||||
@@ -10,8 +11,14 @@ import * as context from './contextStore.js';
|
||||
import * as render from '../renderGovernor.js';
|
||||
import * as layerState from './layerState.js';
|
||||
|
||||
const sourceSlot = createSourceSlot(
|
||||
createSatelliteSource(),
|
||||
['readGroup'],
|
||||
'Satellite source',
|
||||
);
|
||||
export const configureSatelliteSource = sourceSlot.configure;
|
||||
const layer = createSatellitesLayer({
|
||||
source: createSatelliteSource(),
|
||||
source: sourceSlot.source,
|
||||
services: { picking, focus, readout, overlays, context, render, layerState },
|
||||
});
|
||||
export const satelliteVisualsVisible = layer.satelliteVisualsVisible;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { applicationServices } from '../services/application.js';
|
||||
// src/data/terrainHeights.js — batched, cached client terrain-height resolver
|
||||
// (docs/plans/2026-07-05-entity-height-datum-fix.md Task 3).
|
||||
//
|
||||
@@ -101,11 +102,8 @@ export function cachedRealEllipsoidalGround(lat, lon) {
|
||||
async function fetchChunk(chunk) {
|
||||
// lon,lat order (matches the proxy's documented `points=lon,lat;…` contract
|
||||
// and Task 2's implementation).
|
||||
const pointsParam = chunk.map(({ lat, lon }) => `${lon.toFixed(5)},${lat.toFixed(5)}`).join(';');
|
||||
const url = `/api/terrain/heights?points=${encodeURIComponent(pointsParam)}`;
|
||||
const res = await fetch(url, { signal: AbortSignal.timeout(30000) });
|
||||
if (!res.ok) throw new Error(`terrain heights proxy HTTP ${res.status}`);
|
||||
const body = await res.json();
|
||||
const results = await applicationServices.terrain.getHeights(chunk, { signal: AbortSignal.timeout(30000) });
|
||||
const body = { results };
|
||||
if (!Array.isArray(body?.results)) throw new Error('malformed terrain heights response (no results array)');
|
||||
if (body.results.length !== chunk.length) {
|
||||
throw new Error(
|
||||
|
||||
+12
-1
@@ -1,10 +1,21 @@
|
||||
import { createSourceSlot } from '../app/sourceSlot.js';
|
||||
import { createTrafficSource } from '../layers/traffic/source.js';
|
||||
import { createTrafficLayer } from '../layers/traffic/index.js';
|
||||
import * as credits from './dataCredits.js';
|
||||
import * as render from '../renderGovernor.js';
|
||||
|
||||
const sourceSlot = createSourceSlot(
|
||||
createTrafficSource(),
|
||||
['requestRoads', 'getStatus', 'fetchFlowForBounds'],
|
||||
'Traffic source',
|
||||
{
|
||||
getFlowSessionStats: () => ({ tilesFetched: 0 }),
|
||||
resetFlowTileCache: () => {},
|
||||
},
|
||||
);
|
||||
export const configureTrafficSource = sourceSlot.configure;
|
||||
const layer = createTrafficLayer({
|
||||
source: createTrafficSource(),
|
||||
source: sourceSlot.source,
|
||||
services: { credits, render },
|
||||
});
|
||||
export const getTrafficTimingDiagnostics = layer.getTrafficTimingDiagnostics;
|
||||
|
||||
Reference in new issue
Block a user