mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 14:22:21 +00:00
Merge branch 'main' into feat/rio-transit-gtfs-rt
This commit is contained in:
18 files changed
+355
-6
No files matched your search
@@ -196,6 +196,7 @@ export function createModel({ state: layerState, services, parts, source }) {
|
||||
id: cohort.id,
|
||||
label: cohort.label,
|
||||
source: cohort.source,
|
||||
sourceStatus: cohort.sourceStatus || null,
|
||||
coverage: cohort.coverage || null,
|
||||
relationship: cohort.summary.relationship,
|
||||
count: cohort.summary.count,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
t,
|
||||
translateKnown,
|
||||
} from '../../i18n/index.js';
|
||||
import { sourceStatusLabel } from '../../ui/layerNames.js';
|
||||
import { formatQuantity } from '../../ui/unitLabels.js';
|
||||
import { AWARENESS_PAGE_SIZE, AWARENESS_PAGE_ROTATE_MS } from './policy.js';
|
||||
import { distanceParts } from '../../data/distanceFormat.js';
|
||||
@@ -150,7 +151,7 @@ export function createPanel({ state: layerState, parts }) {
|
||||
relationship: summary.relationship.toLowerCase(),
|
||||
label: cohort.label,
|
||||
count: `${presentAwarenessCount(summary)}${pageLabel}`,
|
||||
detail: `${cohort.source}${coverage} · ${presentAwarenessReason(summary)}`,
|
||||
detail: `${sourceStatusLabel(cohort.source, cohort.sourceStatus)}${coverage} · ${presentAwarenessReason(summary)}`,
|
||||
nearest,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createPanel } from './panel.js';
|
||||
|
||||
test('a Contacts row names its provider as the Layers panel does, with its feed state', () => {
|
||||
const cohort = (id, source, sourceStatus) => ({
|
||||
id,
|
||||
label: id,
|
||||
source,
|
||||
sourceStatus,
|
||||
summary: { relationship: 'OBSERVED', count: 1, nearest: [], reason: 'observed' },
|
||||
});
|
||||
const state = {
|
||||
enabled: true,
|
||||
results: {
|
||||
subject: { label: 'AFR38JF' },
|
||||
cohorts: [
|
||||
cohort('flights', 'adsb.lol', 'fallback'),
|
||||
cohort('military', 'adsb.lol', null),
|
||||
cohort('ais-live-vessels', 'Datalastic', 'stale'),
|
||||
],
|
||||
},
|
||||
cohortPages: new Map(),
|
||||
panelListeners: new Set(),
|
||||
panelSignature: '',
|
||||
navigationIndex: 0,
|
||||
};
|
||||
const parts = {
|
||||
navigation: { canNavigateNext: () => false },
|
||||
rendering: { hideDirectionOverlay() {} },
|
||||
};
|
||||
createPanel({ state, parts }).renderResults();
|
||||
assert.deepEqual(
|
||||
state.panelView.rows.map((row) => row.detail),
|
||||
['adsb.lol · FALLBACK · observed', 'adsb.lol · observed', 'Datalastic · STALE · observed'],
|
||||
);
|
||||
state.offPanelLocale?.();
|
||||
});
|
||||
@@ -91,6 +91,7 @@ export function createQueries({ state: layerState, services, parts, source }) {
|
||||
stats.count ?? null,
|
||||
stats.status || null,
|
||||
stats.error || null,
|
||||
stats.source || null,
|
||||
];
|
||||
})
|
||||
.map((parts) => parts.join(':'))
|
||||
|
||||
@@ -81,3 +81,29 @@ test('Contacts excludes square corners and measures from the current moving subj
|
||||
assert.equal(moved.reason, 'mapped matches within 100 km of the subject');
|
||||
assert.deepEqual(installationState.contextAnchor, { latitude: 0, longitude: 0 });
|
||||
});
|
||||
|
||||
test('Contacts cohorts carry the provider and whether it answered as a fallback or stale', () => {
|
||||
const services = { installations: { getNearby: () => [], getStats: () => ({}) },
|
||||
flights: { getNearby: () => [] }, military: { getNearby: () => [] }, vessels: { getNearby: () => [] } };
|
||||
const parts = { navigation: { summarizeAwarenessCohortForNavigation: (items, source) =>
|
||||
summarizeAwarenessCohort(items, source) } };
|
||||
parts.queries = createQueries({ state: {}, services, parts });
|
||||
const subjectOwner = createSubject({ state: {}, services, parts });
|
||||
const states = {
|
||||
// The OpenSky proxy answers from adsb.lol when OpenSky fails.
|
||||
flights: { available: true, stats: { source: 'adsb.lol', coverage: '250nm regional fallback', count: 4, lastUpdate: 1 } },
|
||||
military: { available: true, stats: { source: 'adsb.lol', fallback: false, count: 2, lastUpdate: 1 } },
|
||||
'ais-live-vessels': { available: true, stats: { source: 'Datalastic', stale: true, count: 3, lastUpdate: 1 } },
|
||||
'military-installations': { available: true, stats: { coverage: { kind: 'subject', radiusM: 100000 } } },
|
||||
};
|
||||
const { cohorts } = subjectOwner.evaluateSubject({
|
||||
id: 'subject', layerId: 'flights', position: Cesium.Cartesian3.fromDegrees(0, 0, 10000),
|
||||
}, states);
|
||||
const byId = Object.fromEntries(cohorts.map((cohort) => [cohort.id, cohort]));
|
||||
assert.equal(byId.flights.source, 'adsb.lol');
|
||||
assert.equal(byId.flights.sourceStatus, 'fallback');
|
||||
assert.equal(byId.military.sourceStatus, null);
|
||||
assert.equal(byId['ais-live-vessels'].source, 'Datalastic');
|
||||
assert.equal(byId['ais-live-vessels'].sourceStatus, 'stale');
|
||||
assert.equal(byId['military-installations'].sourceStatus, null);
|
||||
});
|
||||
@@ -0,0 +1,159 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import { createQueries } from './queries.js';
|
||||
import { createSubject } from './subject.js';
|
||||
import { createModel } from './model.js';
|
||||
import { summarizeAwarenessCohort } from '../../data/militaryAwarenessEngine.js';
|
||||
import { layerFeedState } from '../../data/feedState.js';
|
||||
import aisLiveVesselsLayer, {
|
||||
_setVesselStateForTest,
|
||||
_beginAisSessionForTest,
|
||||
_applyAisFeedSnapshotForTest,
|
||||
_setAisRuntimeForTest,
|
||||
} from '../../data/aisLiveVessels.js';
|
||||
|
||||
test('a poll that returns the same stale AIS snapshot restores STALE on Contacts cohorts', () => {
|
||||
globalThis.document ??= { body: null };
|
||||
const services = {
|
||||
installations: { getNearby: () => [], getStats: () => ({}) },
|
||||
flights: { getNearby: () => [] },
|
||||
military: { getNearby: () => [] },
|
||||
vessels: { getNearby: () => [] },
|
||||
};
|
||||
const layerState = {
|
||||
enabled: true,
|
||||
passive: true,
|
||||
sourceRevision: '',
|
||||
cohortPages: new Map(),
|
||||
subject: {
|
||||
id: 'site-1',
|
||||
layerId: 'military-installations',
|
||||
label: 'Site',
|
||||
position: Cesium.Cartesian3.fromDegrees(0, 0, 0),
|
||||
},
|
||||
};
|
||||
let states;
|
||||
const parts = {
|
||||
navigation: {
|
||||
summarizeAwarenessCohortForNavigation: (items, source) =>
|
||||
summarizeAwarenessCohort(items, source),
|
||||
isFlightLayer: (id) => id === 'flights' || id === 'military',
|
||||
},
|
||||
rendering: { renderVisual() {}, scheduleDirectionOverlayUpdate() {} },
|
||||
panel: { renderResults() {} },
|
||||
};
|
||||
const realQueries = createQueries({ state: layerState, services, parts });
|
||||
parts.queries = { ...realQueries, collectSourceStates: () => states };
|
||||
parts.model = createModel({ state: layerState, services, parts });
|
||||
const subject = createSubject({ state: layerState, services, parts });
|
||||
const flights = (lastUpdate) => ({
|
||||
available: true, enabled: true, stale: false,
|
||||
stats: { source: 'OpenSky Network', count: 4, lastUpdate },
|
||||
});
|
||||
const vessels = (loading) => ({
|
||||
available: true, enabled: true, stale: true,
|
||||
stats: { source: 'Datalastic', stale: true, count: 3, lastUpdate: 1000, loading },
|
||||
});
|
||||
const rest = {
|
||||
military: { available: true, enabled: true, stale: false, stats: { source: 'adsb.lol', fallback: false, count: 1, lastUpdate: 1 } },
|
||||
'military-installations': { available: true, enabled: true, stale: false, stats: { coverage: { kind: 'subject', radiusM: 100000 } } },
|
||||
};
|
||||
const ais = () => layerState.results.cohorts.find((c) => c.id === 'ais-live-vessels');
|
||||
|
||||
// 1. Stale AIS cache, nothing loading.
|
||||
states = { flights: flights(1), 'ais-live-vessels': vessels(false), ...rest };
|
||||
subject.refreshSelectedSubject(true);
|
||||
assert.equal(ais().sourceStatus, 'stale');
|
||||
|
||||
// 2. AIS re-poll in flight while the flights poll lands (revision changes).
|
||||
states = { flights: flights(2), 'ais-live-vessels': vessels(true), ...rest };
|
||||
subject.refreshSelectedSubject();
|
||||
assert.equal(ais().sourceStatus, 'stale', 'a poll in flight keeps the snapshot stale');
|
||||
|
||||
// 3. AIS poll returns the same stale snapshot; only loading flips back.
|
||||
states = { flights: flights(2), 'ais-live-vessels': vessels(false), ...rest };
|
||||
subject.refreshSelectedSubject();
|
||||
subject.refreshSelectedSubject();
|
||||
assert.equal(ais().sourceStatus, 'stale', 'Layers reads STALE again; Contacts/Cockpit must too');
|
||||
});
|
||||
|
||||
function fakeRuntime() {
|
||||
return {
|
||||
now: () => 1000,
|
||||
setTimeout: () => ({}),
|
||||
clearTimeout() {},
|
||||
};
|
||||
}
|
||||
|
||||
test('first AIS connect (no snapshot yet) carries no STALE on the Contacts cohort', () => {
|
||||
globalThis.document ??= { body: null };
|
||||
_setAisRuntimeForTest(fakeRuntime());
|
||||
_setVesselStateForTest({ viewer: {}, records: [] });
|
||||
try {
|
||||
_beginAisSessionForTest();
|
||||
// What server/providers/vessels/ais-live.js answers while its socket connects.
|
||||
_applyAisFeedSnapshotForTest({}, {
|
||||
rows: [], source: 'AISStream', status: 'connecting', refreshing: true,
|
||||
});
|
||||
const vesselStats = aisLiveVesselsLayer.getStats();
|
||||
assert.equal(vesselStats.loading, true);
|
||||
assert.equal(vesselStats.stale, true);
|
||||
assert.equal(vesselStats.lastUpdate, null);
|
||||
assert.equal(layerFeedState(vesselStats), 'loading', 'Layers chip reads LOADING');
|
||||
|
||||
const flightsStats = { source: 'OpenSky Network', count: 2, lastUpdate: 5 };
|
||||
const militaryStats = { source: 'adsb.lol', fallback: false, count: 1, lastUpdate: 5 };
|
||||
const installStats = { coverage: { kind: 'subject', radiusM: 100000 } };
|
||||
const statsById = {
|
||||
flights: flightsStats,
|
||||
military: militaryStats,
|
||||
'ais-live-vessels': vesselStats,
|
||||
'military-installations': installStats,
|
||||
};
|
||||
const services = {
|
||||
installations: { getNearby: () => [], getStats: () => installStats },
|
||||
flights: { getNearby: () => [] },
|
||||
military: { getNearby: () => [] },
|
||||
vessels: aisLiveVesselsLayer,
|
||||
};
|
||||
const layerState = {
|
||||
enabled: true,
|
||||
passive: true,
|
||||
sourceRevision: '',
|
||||
cohortPages: new Map(),
|
||||
dataManager: {
|
||||
isEnabled: () => true,
|
||||
getLayerLifecycleState: () => ({ enabled: true, lifecycleState: 'enabled' }),
|
||||
layers: new Map(Object.entries(statsById).map(([id, s]) => [id, { module: { getStats: () => s } }])),
|
||||
},
|
||||
subject: {
|
||||
id: 'site-1',
|
||||
layerId: 'military-installations',
|
||||
label: 'Site',
|
||||
position: Cesium.Cartesian3.fromDegrees(0, 0, 0),
|
||||
},
|
||||
};
|
||||
const parts = {
|
||||
navigation: {
|
||||
summarizeAwarenessCohortForNavigation: (items, source) => summarizeAwarenessCohort(items, source),
|
||||
isFlightLayer: (id) => id === 'flights' || id === 'military',
|
||||
},
|
||||
rendering: { renderVisual() {}, scheduleDirectionOverlayUpdate() {} },
|
||||
panel: { renderResults() {} },
|
||||
};
|
||||
parts.queries = createQueries({ state: layerState, services, parts });
|
||||
parts.model = createModel({ state: layerState, services, parts });
|
||||
const subject = createSubject({ state: layerState, services, parts });
|
||||
|
||||
const vesselsSource = parts.queries.collectSourceStates()['ais-live-vessels'];
|
||||
assert.equal(vesselsSource.available, false, 'never-answered source is unavailable');
|
||||
|
||||
subject.refreshSelectedSubject(true);
|
||||
const ais = layerState.results.cohorts.find((c) => c.id === 'ais-live-vessels');
|
||||
assert.equal(ais.sourceStatus, null, 'no snapshot yet: no STALE label, Layers says LOADING');
|
||||
} finally {
|
||||
_setVesselStateForTest({ enabled: false });
|
||||
_setAisRuntimeForTest();
|
||||
}
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
AWARENESS_RELATIONSHIP,
|
||||
} from '../../data/militaryAwarenessEngine.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import { layerFeedState } from '../../data/feedState.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
import {
|
||||
AWARENESS_QUERY_LIMIT,
|
||||
@@ -52,6 +53,18 @@ export function createSubject({ state: layerState, services, parts, source }) {
|
||||
const vesselsState = sourceStates['ais-live-vessels'];
|
||||
const installationsState = sourceStates['military-installations'];
|
||||
const evaluatedAt = Date.now();
|
||||
// A poll in flight keeps FALLBACK or STALE: the revision key ignores loading.
|
||||
// Before the first answer there is no snapshot, so loading stays.
|
||||
const sourceStatus = (sourceState) => {
|
||||
const feedState = layerFeedState(
|
||||
sourceState.available
|
||||
? { ...sourceState.stats, loading: false }
|
||||
: sourceState.stats,
|
||||
);
|
||||
return feedState === 'fallback' || feedState === 'stale'
|
||||
? feedState
|
||||
: null;
|
||||
};
|
||||
const retainedProvenance = (id, label, sourceState, fallbackSource) => {
|
||||
const retainedSource = sourceState.stats.source || fallbackSource;
|
||||
return {
|
||||
@@ -94,6 +107,7 @@ export function createSubject({ state: layerState, services, parts, source }) {
|
||||
id: 'flights',
|
||||
label: t('Flights'),
|
||||
source: flightsState.stats.source || SOURCE_LABEL.flights,
|
||||
sourceStatus: sourceStatus(flightsState),
|
||||
provenance: retainedProvenance(
|
||||
'flights',
|
||||
'Flights',
|
||||
@@ -113,6 +127,7 @@ export function createSubject({ state: layerState, services, parts, source }) {
|
||||
id: 'military',
|
||||
label: t('Military flights'),
|
||||
source: militaryState.stats.source || SOURCE_LABEL.military,
|
||||
sourceStatus: sourceStatus(militaryState),
|
||||
provenance: retainedProvenance(
|
||||
'military',
|
||||
'Military flights',
|
||||
@@ -132,6 +147,7 @@ export function createSubject({ state: layerState, services, parts, source }) {
|
||||
id: 'ais-live-vessels',
|
||||
label: t('AIS vessels'),
|
||||
source: vesselsState.stats.source || SOURCE_LABEL['ais-live-vessels'],
|
||||
sourceStatus: sourceStatus(vesselsState),
|
||||
provenance: retainedProvenance(
|
||||
'ais-live-vessels',
|
||||
'AIS vessels',
|
||||
@@ -149,6 +165,7 @@ export function createSubject({ state: layerState, services, parts, source }) {
|
||||
source:
|
||||
installationsState.stats.source ||
|
||||
SOURCE_LABEL['military-installations'],
|
||||
sourceStatus: sourceStatus(installationsState),
|
||||
provenance: retainedProvenance(
|
||||
'military-installations',
|
||||
'Mapped installations',
|
||||
|
||||
@@ -409,3 +409,15 @@ test('empty coverage cannot hide stale, incomplete or rejected source data', asy
|
||||
assert.ok(layer.getStats().error, JSON.stringify(patch));
|
||||
}
|
||||
});
|
||||
|
||||
test('vessel stats name the provider the last snapshot reported', async () => {
|
||||
const { layer } = setup({
|
||||
label: 'Vessels',
|
||||
async getSnapshot() {
|
||||
return snapshot([observation('111')], { source: 'Datalastic' });
|
||||
},
|
||||
});
|
||||
assert.equal(layer.getStats().source, 'Vessels');
|
||||
await layer.update();
|
||||
assert.equal(layer.getStats().source, 'Datalastic');
|
||||
});
|
||||
@@ -548,6 +548,8 @@ export function createQueries({
|
||||
// backing off, say how long until the next attempt instead of leaving
|
||||
// the user to guess whether anything is still happening.
|
||||
retryInSec: aisRetryInSec(),
|
||||
// Same field as flights and military, so Contacts names the provider.
|
||||
source: layer.source,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** Contact readouts and nearby-signal presentation for the Cockpit controller. */
|
||||
import * as Cesium from 'cesium';
|
||||
import { formatNumber, t, tn } from '../i18n/index.js';
|
||||
import { sourceStatusLabel } from './layerNames.js';
|
||||
import { unitLabel } from './unitLabels.js';
|
||||
import {
|
||||
bearingBetweenCoordinates,
|
||||
@@ -110,6 +111,7 @@ export function updateContext(info, heading) {
|
||||
if (shown) {
|
||||
shown.count = cohort.count === null ? '?' : String(cohort.count);
|
||||
shown.unknown = cohort.relationship === 'UNKNOWN';
|
||||
shown.source = sourceStatusLabel(cohort.source, cohort.sourceStatus);
|
||||
}
|
||||
if (cohort.count === null) unknownCount += 1;
|
||||
for (const item of cohort.nearest) nearest.push({ ...item, cohort });
|
||||
|
||||
@@ -96,6 +96,13 @@ test('before the controller presents, the card shows the state it starts in', (t
|
||||
),
|
||||
['—', '—', '—', '—'],
|
||||
);
|
||||
// No provider is named before a snapshot reports one.
|
||||
assert.deepEqual(
|
||||
[...document.querySelectorAll('[data-context-cohort] small')].map(
|
||||
(source) => source.textContent,
|
||||
),
|
||||
['—', '—', '—', '—'],
|
||||
);
|
||||
assert.equal(text('#cockpit-context-nearest-label'), 'NO AVAILABLE EXAMPLE');
|
||||
assert.equal(
|
||||
$('#cockpit-context-nearest-label').hasAttribute('aria-label'),
|
||||
@@ -159,6 +166,43 @@ test('a snapshot fills the card and keeps classes other code sets', async (t) =>
|
||||
assert.equal(layouts.length, 1);
|
||||
});
|
||||
|
||||
test('each count names the provider its snapshot reports, with its feed state', async (t) => {
|
||||
const reported = snapshot();
|
||||
reported.cohorts[0].source = 'adsb.lol';
|
||||
reported.cohorts[0].sourceStatus = 'fallback';
|
||||
reported.cohorts[1].source = 'Plane Finder';
|
||||
reported.cohorts.push(
|
||||
{
|
||||
id: 'ais-live-vessels',
|
||||
label: 'AIS vessels',
|
||||
source: 'Datalastic',
|
||||
sourceStatus: 'stale',
|
||||
count: 3,
|
||||
relationship: 'OBSERVED',
|
||||
nearest: [],
|
||||
},
|
||||
{
|
||||
id: 'military-installations',
|
||||
label: 'Mapped installations',
|
||||
source: 'OpenStreetMap',
|
||||
sourceStatus: null,
|
||||
count: 0,
|
||||
relationship: 'MAPPED',
|
||||
nearest: [],
|
||||
},
|
||||
);
|
||||
const { $, owner } = fixture(t, reported);
|
||||
updateContext.call(owner, INFO, 0);
|
||||
await nextTick();
|
||||
const source = (id) => $(`[data-context-cohort="${id}"] small`);
|
||||
assert.equal(source('flights').textContent, 'adsb.lol · FALLBACK');
|
||||
assert.equal(source('military').textContent, 'Plane Finder');
|
||||
assert.equal(source('ais-live-vessels').textContent, 'Datalastic · STALE');
|
||||
assert.equal(source('military-installations').textContent, 'OpenStreetMap');
|
||||
// The tile ellipsizes; the full name stays readable on hover.
|
||||
assert.equal(source('flights').title, 'adsb.lol · FALLBACK');
|
||||
});
|
||||
|
||||
test('a lost contact holds its readout behind the CONTACT LOST cue', async (t) => {
|
||||
const { $, text, owner, layouts } = fixture(t, snapshot());
|
||||
updateContext.call(owner, INFO, 0);
|
||||
|
||||
@@ -25,7 +25,10 @@ export function createContextState() {
|
||||
canPrevious: true,
|
||||
canNext: true,
|
||||
cohorts: Object.fromEntries(
|
||||
COCKPIT_CONTEXT_COHORTS.map((id) => [id, { count: '—', unknown: false }]),
|
||||
COCKPIT_CONTEXT_COHORTS.map((id) => [
|
||||
id,
|
||||
{ count: '—', unknown: false, source: '—' },
|
||||
]),
|
||||
),
|
||||
nearest: { label: t('NO AVAILABLE EXAMPLE'), ariaLabel: null },
|
||||
distance: { text: '—', ariaLabel: null },
|
||||
|
||||
@@ -141,3 +141,17 @@ export function sourceLabel(source) {
|
||||
? source.split(' · ').map(translateKnown).join(' · ')
|
||||
: translateKnown(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* A provider name with the feed state it answered in, so a fallback or stale
|
||||
* snapshot does not read as the live primary feed.
|
||||
* @param {string} source English source name.
|
||||
* @param {'fallback'|'stale'|null} status Feed state from `layerFeedState`.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function sourceStatusLabel(source, status) {
|
||||
const name = sourceLabel(source);
|
||||
if (status === 'fallback') return `${name} · ${t('FALLBACK')}`;
|
||||
if (status === 'stale') return `${name} · ${t('STALE')}`;
|
||||
return name;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
LAYER_NAME_KEYS,
|
||||
PANEL_LABEL_KEYS,
|
||||
sourceLabel,
|
||||
sourceStatusLabel,
|
||||
} from './layerNames.js';
|
||||
|
||||
// Layers whose module name is a proper name and stays as it is in every
|
||||
@@ -51,3 +52,19 @@ for (const locale of ['es', 'ru']) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test('a provider name carries the fallback or stale state it answered in', () => {
|
||||
assert.equal(sourceStatusLabel('Plane Finder', null), 'Plane Finder');
|
||||
assert.equal(sourceStatusLabel('adsb.lol', 'fallback'), 'adsb.lol · FALLBACK');
|
||||
assert.equal(sourceStatusLabel('OpenSky Network', 'stale'), 'OpenSky Network · STALE');
|
||||
setLocale('ru');
|
||||
try {
|
||||
assert.equal(
|
||||
sourceStatusLabel('adsb.lol', 'fallback'),
|
||||
`adsb.lol · ${t('FALLBACK')}`,
|
||||
);
|
||||
assert.notEqual(t('FALLBACK'), 'FALLBACK');
|
||||
} finally {
|
||||
setLocale('en');
|
||||
}
|
||||
});
|
||||
@@ -49,10 +49,10 @@ const weatherLabel = computed(() =>
|
||||
<span class="cockpit-context-qualifier">{{ t('CONTEXT ONLY') }}</span>
|
||||
</header>
|
||||
<div class="cockpit-context-cohorts" role="list" :aria-label="t('Nearby cohort counts')">
|
||||
<div v-class-toggle="{ unknown: c.cohorts.flights.unknown }" role="listitem" data-context-cohort="flights"><span>{{ t('FLT') }}</span><strong>{{ c.cohorts.flights.count }}</strong><small translate="no">OpenSky</small></div>
|
||||
<div v-class-toggle="{ unknown: c.cohorts.military.unknown }" role="listitem" data-context-cohort="military"><span>{{ t('MIL') }}</span><strong>{{ c.cohorts.military.count }}</strong><small translate="no">adsb.lol</small></div>
|
||||
<div v-class-toggle="{ unknown: c.cohorts['ais-live-vessels'].unknown }" role="listitem" data-context-cohort="ais-live-vessels"><span translate="no">AIS</span><strong>{{ c.cohorts['ais-live-vessels'].count }}</strong><small translate="no">AISStream</small></div>
|
||||
<div v-class-toggle="{ unknown: c.cohorts['military-installations'].unknown }" role="listitem" data-context-cohort="military-installations"><span>{{ t('SITE') }}</span><strong>{{ c.cohorts['military-installations'].count }}</strong><small translate="no">OpenStreetMap</small></div>
|
||||
<div v-class-toggle="{ unknown: c.cohorts.flights.unknown }" role="listitem" data-context-cohort="flights"><span>{{ t('FLT') }}</span><strong>{{ c.cohorts.flights.count }}</strong><small translate="no" :title="c.cohorts.flights.source">{{ c.cohorts.flights.source }}</small></div>
|
||||
<div v-class-toggle="{ unknown: c.cohorts.military.unknown }" role="listitem" data-context-cohort="military"><span>{{ t('MIL') }}</span><strong>{{ c.cohorts.military.count }}</strong><small translate="no" :title="c.cohorts.military.source">{{ c.cohorts.military.source }}</small></div>
|
||||
<div v-class-toggle="{ unknown: c.cohorts['ais-live-vessels'].unknown }" role="listitem" data-context-cohort="ais-live-vessels"><span translate="no">AIS</span><strong>{{ c.cohorts['ais-live-vessels'].count }}</strong><small translate="no" :title="c.cohorts['ais-live-vessels'].source">{{ c.cohorts['ais-live-vessels'].source }}</small></div>
|
||||
<div v-class-toggle="{ unknown: c.cohorts['military-installations'].unknown }" role="listitem" data-context-cohort="military-installations"><span>{{ t('SITE') }}</span><strong>{{ c.cohorts['military-installations'].count }}</strong><small translate="no" :title="c.cohorts['military-installations'].source">{{ c.cohorts['military-installations'].source }}</small></div>
|
||||
</div>
|
||||
<div class="cockpit-context-nearest">
|
||||
<span id="cockpit-context-direction" v-class-toggle="{ unknown: c.direction.unknown }" class="material-symbols-outlined" :style="directionStyle" aria-hidden="true">navigation</span>
|
||||
|
||||
Reference in new issue
Block a user