mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
Submarine cables wrote each pick's info both to a per-load WeakMap and to a __gevTeleGeography expando, always together; one cablePicks store replaces both and resolves the same objects. The voice view-target prewarm marks its viewer in a WeakSet instead of a __gevViewTargetPrewarmInstalled property.
237 lines
8.5 KiB
JavaScript
237 lines
8.5 KiB
JavaScript
import { msg, t } from '../../i18n/index.js';
|
|
import {
|
|
featureLabel,
|
|
featureReference,
|
|
normalizeFeatures,
|
|
} from './geometry.js';
|
|
import * as Cesium from 'cesium';
|
|
|
|
const SOURCE_FEATURE_INDEX = '__gevSourceFeatureIndex';
|
|
|
|
function indexedCollection(features) {
|
|
return {
|
|
type: 'FeatureCollection',
|
|
features: features.map((feature, index) => ({
|
|
...feature,
|
|
properties: {
|
|
...feature.properties,
|
|
[SOURCE_FEATURE_INDEX]: index,
|
|
},
|
|
})),
|
|
};
|
|
}
|
|
|
|
export function createIngestion({ state, parts, source }) {
|
|
async function load(viewer) {
|
|
if (state._loading || state._loaded) return;
|
|
|
|
state._loading = true;
|
|
state._error = null;
|
|
state._loadingLabel = t('loading...');
|
|
// Ownership token (the militaryAwareness activationId pattern): a
|
|
// disable/abort followed by a fresh enable starts a NEWER load while this
|
|
// one is still settling. The stale load must bail after every await and
|
|
// must never touch shared lifecycle state or the viewer it no longer
|
|
// owns — clearing `_loading`/`_abort` for the successor is how duplicate
|
|
// or post-destroy data sources got added.
|
|
const generation = ++state._loadGeneration;
|
|
const abort = new AbortController();
|
|
state._abort = abort;
|
|
const owns = () =>
|
|
generation === state._loadGeneration && !abort.signal.aborted;
|
|
|
|
try {
|
|
let cableJson = state._cachedCableJson;
|
|
let landingJson = state._cachedLandingJson;
|
|
if (!cableJson || !landingJson) {
|
|
({ cables: cableJson, landingPoints: landingJson } = await source.fetch(
|
|
abort.signal,
|
|
));
|
|
if (!owns()) return;
|
|
if (
|
|
!Array.isArray(cableJson?.features) ||
|
|
!Array.isArray(landingJson?.features)
|
|
) {
|
|
throw new TypeError(
|
|
'A cable source must return cables and landingPoints GeoJSON collections',
|
|
);
|
|
}
|
|
state._cachedCableJson = cableJson;
|
|
state._cachedLandingJson = landingJson;
|
|
}
|
|
|
|
const cableFeatures = normalizeFeatures(cableJson, 'cable');
|
|
const landingFeatures = normalizeFeatures(landingJson, 'landing');
|
|
|
|
const cableDataSource = await Cesium.GeoJsonDataSource.load(
|
|
indexedCollection(cableFeatures),
|
|
{
|
|
clampToGround: true,
|
|
stroke: state.cableColor.withAlpha(0.95),
|
|
fill: state.cableColor.withAlpha(0.18),
|
|
strokeWidth: 2,
|
|
markerColor: state.cableColor,
|
|
markerSize: 6,
|
|
},
|
|
);
|
|
if (!owns()) return;
|
|
const landingDataSource = await Cesium.GeoJsonDataSource.load(
|
|
indexedCollection(landingFeatures),
|
|
{
|
|
clampToGround: true,
|
|
stroke: state.landingColor.withAlpha(0.9),
|
|
fill: state.landingColor.withAlpha(0.35),
|
|
strokeWidth: 2,
|
|
markerColor: state.landingColor,
|
|
markerSize: 6,
|
|
},
|
|
);
|
|
if (!owns()) return;
|
|
|
|
cableDataSource.name = `${source.label} Submarine Cables`;
|
|
landingDataSource.name = `${source.label} Landing Points`;
|
|
const referenceDataSource = new Cesium.CustomDataSource(
|
|
`${source.label} Cable References`,
|
|
);
|
|
const addedSources = [
|
|
cableDataSource,
|
|
landingDataSource,
|
|
referenceDataSource,
|
|
];
|
|
|
|
// dataSources.add() is itself an await point: Cesium mutates the
|
|
// collection on a DEFERRED tick, so a disable/destroy racing this
|
|
// window finds nothing to remove and the adds still materialize
|
|
// afterwards. Await every add to settlement (allSettled so a partial
|
|
// failure cannot leave an unawaited add materializing later), then
|
|
// re-check ownership. Pinned teardown mechanism: disable/destroy never
|
|
// wait for in-flight adds — the stale generation's own post-await
|
|
// check here guarantees cleanup by removing exactly the sources THIS
|
|
// generation added.
|
|
const addResults = await Promise.allSettled([
|
|
viewer.dataSources.add(cableDataSource),
|
|
viewer.dataSources.add(landingDataSource),
|
|
viewer.dataSources.add(referenceDataSource),
|
|
]);
|
|
const rejectedAdd = addResults.find(
|
|
(result) => result.status === 'rejected',
|
|
);
|
|
if (!owns() || rejectedAdd) {
|
|
// Compensate: every add has settled by now, so removal is effective
|
|
// (a remove inside the deferred window would have been a no-op).
|
|
// Remove EVERY generation-local source regardless of settle status:
|
|
// Cesium's add() pushes into the collection BEFORE raising
|
|
// dataSourceAdded, so a REJECTED add may still have landed its
|
|
// mutation. remove() of a never-added source is a harmless no-op.
|
|
for (const source of addedSources) {
|
|
try {
|
|
viewer.dataSources.remove(source, true);
|
|
} catch {
|
|
/* collection gone */
|
|
}
|
|
}
|
|
if (owns() && rejectedAdd) throw rejectedAdd.reason;
|
|
return;
|
|
}
|
|
|
|
// Await-free commit: the viewer accepted all three sources and this
|
|
// load still owns the lifecycle.
|
|
state._cableDataSource = cableDataSource;
|
|
state._landingDataSource = landingDataSource;
|
|
state._referenceDataSource = referenceDataSource;
|
|
|
|
const cableEntities = state._cableDataSource.entities.values;
|
|
const landingEntities = state._landingDataSource.entities.values;
|
|
state._referenceRecords = [];
|
|
state._surfaceRecords = [];
|
|
|
|
// Cesium expands multipart geometry into several entities and suffixes
|
|
// colliding IDs. Its copied properties retain this source index even
|
|
// when a provider has no ID, so every segment keeps the right owner.
|
|
const cableReferences = new Set();
|
|
cableEntities.forEach((entity) => {
|
|
const index = entity.properties?.[SOURCE_FEATURE_INDEX]?.getValue();
|
|
const feature = cableFeatures[index];
|
|
const reference = featureReference(feature);
|
|
if (!reference) return;
|
|
|
|
parts.rendering.styleCableEntity(entity, feature);
|
|
parts.interaction.registerPickEntity(entity, {
|
|
kind: 'cable',
|
|
reference,
|
|
label: featureLabel(feature),
|
|
});
|
|
state._surfaceRecords.push({
|
|
entity,
|
|
base: Cesium.Cartesian3.fromDegrees(reference.lon, reference.lat, 0),
|
|
});
|
|
if (cableReferences.has(index)) return;
|
|
cableReferences.add(index);
|
|
parts.rendering.addReferenceStem({
|
|
reference,
|
|
label: featureLabel(feature),
|
|
kind: 'cable',
|
|
color: state.cableColor,
|
|
feature,
|
|
});
|
|
});
|
|
|
|
const landingReferences = new Set();
|
|
landingEntities.forEach((entity) => {
|
|
const index = entity.properties?.[SOURCE_FEATURE_INDEX]?.getValue();
|
|
const feature = landingFeatures[index];
|
|
const reference = featureReference(feature);
|
|
if (!reference) return;
|
|
|
|
parts.rendering.styleLandingEntity(entity, feature);
|
|
parts.interaction.registerPickEntity(entity, {
|
|
kind: 'landing-point',
|
|
reference,
|
|
label: featureLabel(feature),
|
|
});
|
|
state._surfaceRecords.push({
|
|
entity,
|
|
base: Cesium.Cartesian3.fromDegrees(reference.lon, reference.lat, 0),
|
|
});
|
|
if (landingReferences.has(index)) return;
|
|
landingReferences.add(index);
|
|
parts.rendering.addReferenceStem({
|
|
reference,
|
|
label: featureLabel(feature),
|
|
kind: 'landing-point',
|
|
color: state.landingColor,
|
|
feature,
|
|
});
|
|
});
|
|
|
|
state._count = cableFeatures.length + landingFeatures.length;
|
|
state._loaded = true;
|
|
state._lastUpdate = Date.now();
|
|
state._loadingLabel = '';
|
|
parts.rendering.updateVisibility();
|
|
state._referenceSweepGate.markDirty();
|
|
viewer.scene.requestRender?.();
|
|
} catch (error) {
|
|
// A stale or aborted load reports nothing: its failure belongs to a
|
|
// lifecycle the user already left.
|
|
if (owns() && error?.name !== 'AbortError') {
|
|
state._error = error?.message || msg('TeleGeography load failed');
|
|
console.warn(
|
|
'[Data:telegeography-submarine-cables]',
|
|
state._error,
|
|
error,
|
|
);
|
|
}
|
|
} finally {
|
|
// Only the OWNING load may clear the shared lifecycle. A stale load
|
|
// clearing `_loading`/`_abort` would hand a later enable/update a
|
|
// duplicate load while the real one is still in flight.
|
|
if (generation === state._loadGeneration) {
|
|
state._loading = false;
|
|
if (state._abort === abort) state._abort = null;
|
|
}
|
|
}
|
|
}
|
|
return { load };
|
|
}
|