/**
* Current Route tab — shows the primary lane's chokepoints, transit/freight
* estimates, disruption score, and war risk tier. Shows a noModeledLane
* empty state when the origin/destination clusters have no shared route.
*/
import type {
GetRouteExplorerLaneResponse,
ChokepointExposureSummary,
} from '@/generated/server/worldmonitor/supply_chain/v1/service_server';
import {
formatTransitRange,
formatFreightRange,
formatExposurePct,
formatDisruptionScore,
disruptionScoreClass,
warRiskTierLabel,
warRiskTierClass,
escapeHtml,
} from './route-utils';
import { setTrustedHtml, trustedHtml } from '@/utils/dom-utils';
export interface CurrentRouteTabOptions {
onChokepointSelect?: (chokepointId: string) => void;
}
export class CurrentRouteTab {
public readonly element: HTMLDivElement;
private opts: CurrentRouteTabOptions;
constructor(opts: CurrentRouteTabOptions = {}) {
this.opts = opts;
this.element = document.createElement('div');
this.element.className = 're-tab re-tab--current';
this.element.setAttribute('role', 'tabpanel');
this.renderEmpty();
}
public update(data: GetRouteExplorerLaneResponse | null): void {
if (!data || data.noModeledLane) {
this.renderNoModeledLane();
return;
}
this.renderData(data);
}
private renderEmpty(): void {
setTrustedHtml(this.element, trustedHtml('
Pick a country pair and product to see the current route.
', "legacy direct innerHTML migration"));
}
private renderNoModeledLane(): void {
setTrustedHtml(this.element, trustedHtml('' +
'
No modeled lane
' +
'
WorldMonitor does not have a modeled maritime route between these two countries. ' +
'This may mean the pair shares no major trade corridor in our dataset, or one country is landlocked.
' +
'
', "legacy direct innerHTML migration"));
}
private renderData(data: GetRouteExplorerLaneResponse): void {
const summaryHtml = this.renderSummary(data);
const chokepointsHtml = this.renderChokepointList(data.chokepointExposures);
setTrustedHtml(this.element, trustedHtml(`${summaryHtml}${chokepointsHtml}`, "legacy direct innerHTML migration"));
this.attachChokepointListeners();
}
private renderSummary(data: GetRouteExplorerLaneResponse): string {
const riskCls = warRiskTierClass(data.warRiskTier);
const disruptCls = disruptionScoreClass(data.disruptionScore);
return [
'',
`
`,
` Transit`,
` ${formatTransitRange(data.estTransitDaysRange)}`,
`
`,
`
`,
` Freight (est.)`,
` ${formatFreightRange(data.estFreightUsdPerTeuRange, data.cargoType)}`,
`
`,
`
`,
` Disruption`,
` ${formatDisruptionScore(data.disruptionScore)}`,
`
`,
`
`,
` War Risk`,
` ${escapeHtml(warRiskTierLabel(data.warRiskTier))}`,
`
`,
'
',
].join('\n');
}
private renderChokepointList(exposures: ChokepointExposureSummary[]): string {
if (exposures.length === 0) {
return 'No chokepoint exposures on this route.
';
}
const rows = exposures.map(
(e, i) =>
`` +
`| ${i + 1} | ` +
`${escapeHtml(e.chokepointName)} | ` +
`${formatExposurePct(e.exposurePct)} | ` +
`
`,
);
return [
'',
' | # | Chokepoint | Exposure |
',
` ${rows.join('')}`,
'
',
].join('\n');
}
private attachChokepointListeners(): void {
const rows = this.element.querySelectorAll('.re-current__cp-row');
rows.forEach((row) => {
const cpId = row.dataset.cpId;
if (!cpId) return;
const select = () => this.opts.onChokepointSelect?.(cpId);
row.addEventListener('click', select);
row.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); select(); }
});
});
}
}