Files
gods-eye-view/src/layers/installations/controls.js
T
German KhokhlovandClaude Opus 5.5 20f4cf3763 i18n: fix the LGTM review findings and tidy the branch texts
- i18n scanner: lastError sink, no false sinks on defaults and ternaries
- shared src/i18n/marker.js for modules that only mark strings
- wind field messages move to wind/fields.js; CITY_POIS imports from cityPois.js
- drop review IDs, change history and stale references from comments and docs

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-16 21:00:28 +04:00

276 lines
9.5 KiB
JavaScript

import * as Cesium from 'cesium';
import { describeInstallationFeedback } from '../../data/installationFeedback.js';
import {
LAYER_ID,
DISTANCE_PREFILTER_MARGIN_M,
ANCHOR_REFRESH_M,
} from './policy.js';
import { formatNumber, t, tn } from '../../i18n/index.js';
// installationFeedback.js is shared with the HUD (src/loadingFeedback.js) and
// hands back msg()-marked English plus values; getStats translates them here.
function localizeInstallationFeedback(feedback) {
if (feedback.text !== undefined) {
const values = {};
if (feedback.reason !== undefined) values.reason = t(feedback.reason);
if (feedback.seconds !== undefined) values.seconds = feedback.seconds;
return t(feedback.text, values);
}
const { count, km } = feedback;
if (km) {
const kmText = formatNumber(km);
return count === 0
? t('No mapped sites within {km} km of the contact', { km: kmText })
: tn(
count,
'{count} mapped site within {km} km of the contact',
'{count} mapped sites within {km} km of the contact',
{ km: kmText },
);
}
return count === 0
? t('No mapped sites in view')
: tn(count, '{count} mapped site in view', '{count} mapped sites in view');
}
export function createControls({ state: layerState, services, parts, source }) {
const methods = {
id: LAYER_ID,
name: 'Mapped Installations',
icon: '⌖',
source: 'OpenStreetMap + optional Google Maps Places',
updateInterval: 0,
statsRefreshInterval: 1000,
visitNamedMarkers(visitor) {
parts.namedMarkers.visit(visitor);
},
/** Request a one-shot Google Maps Places search around the current map view. */
searchNearby() {
layerState.googleSearchRequested = true;
return parts.ingestion.loadInstallations();
},
/**
* Load sites around a Contacts subject instead of the camera viewport.
*
* A follow or Cockpit camera moves every frame, so `moveEnd` never fires and
* its view rectangle often reaches the horizon; a subject window is the
* only area that answers "what is near this contact". The window moves
* only after the subject travels ANCHOR_REFRESH_M, and cached tiles make
* that cheap. Passing null returns the layer to viewport loading.
* @param {Cesium.Cartesian3|{latitude:number, longitude:number}|null} position
* Subject position.
* @returns {boolean} Whether a new load was scheduled.
*/
setContextAnchor(position) {
let next = null;
if (position) {
if (
Number.isFinite(position.latitude) &&
Number.isFinite(position.longitude)
)
next = { latitude: position.latitude, longitude: position.longitude };
else {
const carto = Cesium.Cartographic.fromCartesian(position);
if (carto)
next = {
latitude: Cesium.Math.toDegrees(carto.latitude),
longitude: Cesium.Math.toDegrees(carto.longitude),
};
}
}
// Keep the live centre even when the coarse fetch anchor has not moved.
layerState.contextPosition = next
? Cesium.Cartesian3.fromDegrees(next.longitude, next.latitude)
: null;
const previous = layerState.contextAnchor;
if (!next) {
if (!previous) return false;
layerState.contextAnchor = null;
parts.viewport.scheduleLoad();
return true;
}
if (
previous &&
parts.model.approximateSurfaceDistanceM(
Cesium.Math.toRadians(previous.latitude),
Cesium.Math.toRadians(previous.longitude),
next.latitude,
next.longitude,
) < ANCHOR_REFRESH_M
)
return false;
layerState.contextAnchor = next;
parts.viewport.scheduleLoad();
return true;
},
getNearby(center, rangeM, maxCount = 50) {
if (!center) return [];
const range = Number.isFinite(rangeM) ? rangeM : Infinity;
const centerCartographic = Cesium.Cartographic.fromCartesian(center);
if (!centerCartographic) return [];
const nearby = [];
const approximateLimit = Number.isFinite(range)
? range * 1.03 + DISTANCE_PREFILTER_MARGIN_M
: Infinity;
for (const record of layerState.records) {
if (record.kind !== 'installation') continue;
if (
parts.model.approximateSurfaceDistanceM(
centerCartographic.latitude,
centerCartographic.longitude,
record.latitude,
record.longitude,
) > approximateLimit
)
continue;
// The awareness disk is projected onto the ground. Confirm candidates
// with an exact ellipsoidal surface distance and reusable scratch state.
layerState.distanceEndpointScratch.longitude = Cesium.Math.toRadians(
record.longitude,
);
layerState.distanceEndpointScratch.latitude = Cesium.Math.toRadians(
record.latitude,
);
layerState.distanceEndpointScratch.height = 0;
layerState.distanceGeodesicScratch.setEndPoints(
centerCartographic,
layerState.distanceEndpointScratch,
);
const distanceM = layerState.distanceGeodesicScratch.surfaceDistance;
if (!Number.isFinite(distanceM) || distanceM > range) continue;
nearby.push({
...record,
position: Cesium.Cartesian3.fromDegrees(
record.longitude,
record.latitude,
parts.rendering.installationSurfaceHeightM(record),
),
distanceM,
});
}
nearby.sort((a, b) => a.distanceM - b.distanceM);
return nearby.slice(
0,
Number.isFinite(maxCount) ? Math.max(1, Math.floor(maxCount)) : 50,
);
},
/**
* Select and frame a mapped installation from another contextual UI.
* @param {string} id Source-backed installation id.
* @returns {boolean} True when an available installation was focused.
*/
focusById(id) {
const record = layerState.recordById.get(String(id));
if (!record || !layerState.viewer) return false;
// No camera flight without a real selection: a flight plus a stale subject
// reads as success to Context navigation and strands NEXT on this item.
if (!parts.selection.selectRecord(record.id)) return false;
layerState.viewer.camera.flyToBoundingSphere(
new Cesium.BoundingSphere(
Cesium.Cartesian3.fromDegrees(
record.longitude,
record.latitude,
parts.rendering.installationSurfaceHeightM(record),
),
18000,
),
{ duration: 1.4 },
);
return true;
},
/**
* Snapshot loaded mapped installations as plain JSON-safe records for the analyst
* query engine. On demand only (once per spoken query); [] while the
* layer is off.
* @param {number} [maxCount=Infinity] Most records to return.
* @returns {Array<Object>} {id, name, class, kind, lat, lon}.
*/
getAnalystRecords(maxCount = Infinity) {
if (!layerState.enabled) return [];
const result = [];
for (const record of layerState.records || []) {
if (result.length >= maxCount) break;
if (
!Number.isFinite(record.latitude) ||
!Number.isFinite(record.longitude)
)
continue;
result.push({
id: record.name || record.id,
name: record.name || null,
class: record.class || null,
kind: record.kind || null,
lat: record.latitude,
lon: record.longitude,
});
}
return result;
},
getStats() {
const namedMarkers = parts.namedMarkers?.stats();
const subjectWindow =
layerState.coverage?.kind === 'subject' &&
layerState.contextPosition &&
Number.isFinite(layerState.coverage.radiusM);
const mappedCount = subjectWindow
? methods.getNearby(
layerState.contextPosition,
layerState.coverage.radiusM,
layerState.records.length || 1,
).length
: layerState.wide && namedMarkers
? namedMarkers.pointsOnScreen
: layerState.records.filter(
(record) => record.kind === 'installation',
).length;
return {
count:
subjectWindow || layerState.wide
? mappedCount
: layerState.records.length,
namedInView: layerState.namedInView || 0,
wide: Boolean(layerState.wide),
...namedMarkers,
lastUpdate: layerState.lastUpdate,
stale: layerState.stale,
saturated: layerState.saturated,
error: layerState.error,
status: layerState.status,
loading: layerState.loading,
retryAt: layerState.retryAt,
retrying: layerState.loading && Boolean(layerState.failureReason),
failureReason: layerState.failureReason,
statusMessage: localizeInstallationFeedback(
describeInstallationFeedback({
...layerState,
count: layerState.lastUpdate ? mappedCount : undefined,
retrying: layerState.loading && Boolean(layerState.failureReason),
}),
),
loadingLabel: layerState.loading
? t('loading mapped installation context')
: '',
coverage: layerState.coverage,
coverageLabel:
layerState.coverage?.kind === 'subject'
? `WITHIN ${Math.round(layerState.coverage.radiusM / 1000)} KM`
: 'CURRENT VIEWPORT ONLY',
};
},
};
return { methods };
}