| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { PIPELINES } from './pipelines-data'; |
| import { PORTS } from './ports-data'; |
| import { haversineKm } from './geo-distance'; |
| import type { PipelineRecord } from './pipelines-data'; |
| import type { PortRecord } from './ports-data'; |
|
|
| export type InfrastructureNodeType = 'cable' | 'pipeline' | 'port' | 'chokepoint' | 'country' | 'route'; |
|
|
| export interface InfrastructureNode { |
| id: string; |
| type: InfrastructureNodeType; |
| name: string; |
| coordinates?: [number, number]; |
| metadata?: Record<string, unknown>; |
| } |
|
|
| export type DependencyType = |
| | 'serves' |
| | 'terminates_at' |
| | 'transits_through' |
| | 'lands_at' |
| | 'depends_on' |
| | 'shares_risk' |
| | 'alternative_to' |
| | 'trade_route' |
| | 'controls_access' |
| | 'trade_dependency'; |
|
|
| export interface DependencyEdge { |
| from: string; |
| to: string; |
| type: DependencyType; |
| strength: number; |
| redundancy?: number; |
| metadata?: { |
| capacityShare?: number; |
| alternativeRoutes?: number; |
| estimatedImpact?: string; |
| portType?: string; |
| relationship?: string; |
| }; |
| } |
|
|
| export type CascadeImpactLevel = 'critical' | 'high' | 'medium' | 'low'; |
|
|
| export interface CascadeAffectedNode { |
| node: InfrastructureNode; |
| impactLevel: CascadeImpactLevel; |
| pathLength: number; |
| dependencyChain: string[]; |
| redundancyAvailable: boolean; |
| estimatedRecovery?: string; |
| } |
|
|
| export interface CascadeCountryImpact { |
| country: string; |
| countryName: string; |
| impactLevel: CascadeImpactLevel; |
| affectedCapacity: number; |
| criticalSectors?: string[]; |
| } |
|
|
| export interface CascadeResult { |
| source: InfrastructureNode; |
| affectedNodes: CascadeAffectedNode[]; |
| countriesAffected: CascadeCountryImpact[]; |
| economicImpact?: { |
| dailyTradeLoss?: number; |
| affectedThroughput?: number; |
| }; |
| redundancies?: { |
| id: string; |
| name: string; |
| capacityShare: number; |
| }[]; |
| } |
|
|
| |
| export interface CableInput { |
| id: string; |
| name: string; |
| points?: [number, number][]; |
| landingPoints?: { country: string; countryName?: string; city?: string; lat?: number; lon?: number }[]; |
| countriesServed?: { country: string; capacityShare: number; isRedundant?: boolean }[]; |
| capacityTbps?: number; |
| rfsYear?: number; |
| owners?: string[]; |
| } |
|
|
| |
| export interface WaterwayInput { |
| id: string; |
| name: string; |
| lat: number; |
| lon: number; |
| description?: string; |
| } |
|
|
| export type PipelineInput = PipelineRecord; |
| export type PortInput = PortRecord; |
|
|
| export interface CascadeGraphInputs { |
| cables: CableInput[]; |
| waterways: WaterwayInput[]; |
| pipelines?: PipelineInput[]; |
| ports?: PortInput[]; |
| } |
|
|
| export interface DependencyGraph { |
| nodes: Map<string, InfrastructureNode>; |
| edges: DependencyEdge[]; |
| outgoing: Map<string, DependencyEdge[]>; |
| incoming: Map<string, DependencyEdge[]>; |
| |
| cables: CableInput[]; |
| } |
|
|
| |
| const COUNTRY_NAMES: Record<string, string> = { |
| US: 'United States', GB: 'United Kingdom', ES: 'Spain', FR: 'France', |
| DE: 'Germany', IT: 'Italy', PT: 'Portugal', NO: 'Norway', DK: 'Denmark', |
| NL: 'Netherlands', BE: 'Belgium', SE: 'Sweden', FI: 'Finland', IE: 'Ireland', |
| AT: 'Austria', CH: 'Switzerland', GR: 'Greece', CZ: 'Czech Republic', |
| JP: 'Japan', CN: 'China', TW: 'Taiwan', HK: 'Hong Kong', SG: 'Singapore', |
| KR: 'South Korea', AU: 'Australia', NZ: 'New Zealand', IN: 'India', PK: 'Pakistan', |
| AE: 'UAE', SA: 'Saudi Arabia', EG: 'Egypt', KW: 'Kuwait', BH: 'Bahrain', |
| OM: 'Oman', QA: 'Qatar', IR: 'Iran', IQ: 'Iraq', TR: 'Turkey', IL: 'Israel', |
| JO: 'Jordan', LB: 'Lebanon', SY: 'Syria', YE: 'Yemen', |
| NG: 'Nigeria', ZA: 'South Africa', KE: 'Kenya', TZ: 'Tanzania', |
| MZ: 'Mozambique', MG: 'Madagascar', SN: 'Senegal', GH: 'Ghana', |
| CI: 'Ivory Coast', AO: 'Angola', ET: 'Ethiopia', UG: 'Uganda', |
| BR: 'Brazil', AR: 'Argentina', CL: 'Chile', |
| PE: 'Peru', CO: 'Colombia', MX: 'Mexico', PA: 'Panama', VE: 'Venezuela', |
| IS: 'Iceland', FO: 'Faroe Islands', FJ: 'Fiji', ID: 'Indonesia', |
| VN: 'Vietnam', TH: 'Thailand', MY: 'Malaysia', PH: 'Philippines', |
| RU: 'Russia', UA: 'Ukraine', PL: 'Poland', RO: 'Romania', HU: 'Hungary', |
| CA: 'Canada', DJ: 'Djibouti', BD: 'Bangladesh', LK: 'Sri Lanka', MM: 'Myanmar', |
| }; |
|
|
| function addCablesAsNodes(graph: DependencyGraph, cables: CableInput[]): void { |
| for (const cable of cables) { |
| const firstPoint = cable.points?.[0]; |
| graph.nodes.set(`cable:${cable.id}`, { |
| id: `cable:${cable.id}`, |
| type: 'cable', |
| name: cable.name, |
| coordinates: firstPoint ? [firstPoint[0], firstPoint[1]] : undefined, |
| metadata: { |
| capacityTbps: cable.capacityTbps, |
| rfsYear: cable.rfsYear, |
| owners: cable.owners, |
| landingPoints: cable.landingPoints, |
| }, |
| }); |
| } |
| } |
|
|
| function addPipelinesAsNodes(graph: DependencyGraph, pipelines: PipelineInput[]): void { |
| for (const pipeline of pipelines) { |
| const firstPoint = pipeline.points?.[0]; |
| graph.nodes.set(`pipeline:${pipeline.id}`, { |
| id: `pipeline:${pipeline.id}`, |
| type: 'pipeline', |
| name: pipeline.name, |
| coordinates: firstPoint ? [firstPoint[0], firstPoint[1]] : undefined, |
| metadata: { |
| type: pipeline.type, |
| status: pipeline.status, |
| capacity: pipeline.capacity, |
| operator: pipeline.operator, |
| countries: pipeline.countries, |
| }, |
| }); |
| } |
| } |
|
|
| function addPortsAsNodes(graph: DependencyGraph, ports: PortInput[]): void { |
| for (const port of ports) { |
| graph.nodes.set(`port:${port.id}`, { |
| id: `port:${port.id}`, |
| type: 'port', |
| name: port.name, |
| coordinates: [port.lon, port.lat], |
| metadata: { |
| country: port.country, |
| type: port.type, |
| rank: port.rank, |
| }, |
| }); |
| } |
| } |
|
|
| function addChokepointsAsNodes(graph: DependencyGraph, waterways: WaterwayInput[]): void { |
| for (const waterway of waterways) { |
| graph.nodes.set(`chokepoint:${waterway.id}`, { |
| id: `chokepoint:${waterway.id}`, |
| type: 'chokepoint', |
| name: waterway.name, |
| coordinates: [waterway.lon, waterway.lat], |
| metadata: { |
| description: waterway.description, |
| }, |
| }); |
| } |
| } |
|
|
| function addCountriesAsNodes( |
| graph: DependencyGraph, |
| cables: CableInput[], |
| pipelines: PipelineInput[], |
| ): void { |
| const countries = new Set<string>(); |
|
|
| for (const cable of cables) { |
| cable.countriesServed?.forEach(c => countries.add(c.country)); |
| cable.landingPoints?.forEach(lp => countries.add(lp.country)); |
| } |
|
|
| for (const pipeline of pipelines) { |
| pipeline.countries?.forEach(c => { |
| const code = c === 'USA' ? 'US' : c === 'Canada' ? 'CA' : c; |
| countries.add(code); |
| }); |
| } |
|
|
| for (const code of countries) { |
| graph.nodes.set(`country:${code}`, { |
| id: `country:${code}`, |
| type: 'country', |
| name: COUNTRY_NAMES[code] || code, |
| metadata: { code }, |
| }); |
| } |
| } |
|
|
| function addEdge(graph: DependencyGraph, edge: DependencyEdge): void { |
| graph.edges.push(edge); |
|
|
| if (!graph.outgoing.has(edge.from)) graph.outgoing.set(edge.from, []); |
| graph.outgoing.get(edge.from)!.push(edge); |
|
|
| if (!graph.incoming.has(edge.to)) graph.incoming.set(edge.to, []); |
| graph.incoming.get(edge.to)!.push(edge); |
| } |
|
|
| function buildCableCountryEdges(graph: DependencyGraph, cables: CableInput[]): void { |
| for (const cable of cables) { |
| const cableId = `cable:${cable.id}`; |
|
|
| cable.countriesServed?.forEach(cs => { |
| const countryId = `country:${cs.country}`; |
| addEdge(graph, { |
| from: cableId, |
| to: countryId, |
| type: 'serves', |
| strength: cs.capacityShare, |
| redundancy: cs.isRedundant ? 0.5 : 0, |
| metadata: { |
| capacityShare: cs.capacityShare, |
| estimatedImpact: cs.isRedundant ? 'Medium - redundancy available' : 'High - limited redundancy', |
| }, |
| }); |
| }); |
|
|
| cable.landingPoints?.forEach(lp => { |
| const countryId = `country:${lp.country}`; |
| addEdge(graph, { |
| from: cableId, |
| to: countryId, |
| type: 'lands_at', |
| strength: 0.3, |
| redundancy: 0.5, |
| }); |
| }); |
| } |
| } |
|
|
| function buildPipelineCountryEdges(graph: DependencyGraph, pipelines: PipelineInput[]): void { |
| for (const pipeline of pipelines) { |
| const pipelineId = `pipeline:${pipeline.id}`; |
|
|
| pipeline.countries?.forEach(country => { |
| const code = country === 'USA' ? 'US' : country === 'Canada' ? 'CA' : country; |
| const countryId = `country:${code}`; |
|
|
| if (graph.nodes.has(countryId)) { |
| addEdge(graph, { |
| from: pipelineId, |
| to: countryId, |
| type: 'serves', |
| strength: 0.2, |
| redundancy: 0.3, |
| }); |
| } |
| }); |
| } |
| } |
|
|
| |
| function normalizeCountryCode(country: string): string { |
| const mappings: Record<string, string> = { |
| 'USA': 'US', 'China': 'CN', 'China (SAR)': 'CN', 'Taiwan': 'TW', |
| 'South Korea': 'KR', 'Netherlands': 'NL', 'Belgium': 'BE', |
| 'Malaysia': 'MY', 'Thailand': 'TH', 'Greece': 'GR', |
| 'Saudi Arabia': 'SA', 'Iran': 'IR', 'Qatar': 'QA', 'Russia': 'RU', |
| 'Egypt': 'EG', 'UK (Gibraltar)': 'GB', 'Djibouti': 'DJ', |
| 'Yemen': 'YE', 'Panama': 'PA', 'Spain': 'ES', 'Pakistan': 'PK', |
| 'Sri Lanka': 'LK', 'Japan': 'JP', 'UK': 'GB', 'France': 'FR', |
| 'Brazil': 'BR', 'India': 'IN', 'Singapore': 'SG', 'Germany': 'DE', |
| 'UAE': 'AE', |
| }; |
| return mappings[country] || country; |
| } |
|
|
| |
| function getPortImportance(port: PortInput): number { |
| const typeWeight: Record<string, number> = { |
| 'oil': 0.9, |
| 'lng': 0.85, |
| 'container': 0.7, |
| 'mixed': 0.6, |
| 'bulk': 0.5, |
| 'naval': 0.4, |
| }; |
| const baseWeight = typeWeight[port.type] || 0.5; |
| |
| const rankBoost = port.rank ? Math.max(0, (20 - port.rank) / 20) * 0.3 : 0; |
| return Math.min(1, baseWeight + rankBoost); |
| } |
|
|
| function buildPortCountryEdges(graph: DependencyGraph, ports: PortInput[]): void { |
| for (const port of ports) { |
| const portId = `port:${port.id}`; |
| const countryCode = normalizeCountryCode(port.country); |
| const countryId = `country:${countryCode}`; |
|
|
| |
| if (!graph.nodes.has(countryId)) { |
| graph.nodes.set(countryId, { |
| id: countryId, |
| type: 'country', |
| name: COUNTRY_NAMES[countryCode] || port.country, |
| metadata: { code: countryCode }, |
| }); |
| } |
|
|
| const importance = getPortImportance(port); |
|
|
| |
| addEdge(graph, { |
| from: portId, |
| to: countryId, |
| type: 'serves', |
| strength: importance, |
| redundancy: port.rank && port.rank <= 5 ? 0.2 : 0.4, |
| metadata: { |
| portType: port.type, |
| estimatedImpact: importance > 0.7 ? 'Critical port for country' : 'Regional port', |
| }, |
| }); |
|
|
| |
| |
| const affectedCountries = getAffectedCountries(port); |
| for (const affected of affectedCountries) { |
| const affectedCountryId = `country:${affected.code}`; |
| if (!graph.nodes.has(affectedCountryId)) { |
| graph.nodes.set(affectedCountryId, { |
| id: affectedCountryId, |
| type: 'country', |
| name: COUNTRY_NAMES[affected.code] || affected.code, |
| metadata: { code: affected.code }, |
| }); |
| } |
| addEdge(graph, { |
| from: portId, |
| to: affectedCountryId, |
| type: 'trade_route', |
| strength: affected.strength, |
| redundancy: 0.5, |
| metadata: { |
| relationship: affected.reason, |
| }, |
| }); |
| } |
| } |
| } |
|
|
| |
| function getAffectedCountries(port: PortInput): { code: string; strength: number; reason: string }[] { |
| const affected: { code: string; strength: number; reason: string }[] = []; |
|
|
| |
| if (port.id === 'port_said' || port.id === 'suez_port') { |
| affected.push( |
| { code: 'DE', strength: 0.6, reason: 'Major EU importer via Suez' }, |
| { code: 'GB', strength: 0.5, reason: 'UK-Asia trade' }, |
| { code: 'NL', strength: 0.5, reason: 'Rotterdam connection' }, |
| { code: 'CN', strength: 0.4, reason: 'China-EU trade route' }, |
| { code: 'IT', strength: 0.4, reason: 'Mediterranean trade' }, |
| ); |
| } |
|
|
| |
| if (port.id === 'bandar_abbas' || port.id === 'fujairah' || port.id === 'ras_tanura') { |
| affected.push( |
| { code: 'JP', strength: 0.7, reason: 'Oil import dependency' }, |
| { code: 'KR', strength: 0.6, reason: 'Oil import dependency' }, |
| { code: 'IN', strength: 0.5, reason: 'Oil imports' }, |
| { code: 'CN', strength: 0.5, reason: 'Oil imports' }, |
| ); |
| } |
|
|
| |
| if (port.id === 'singapore' || port.id === 'klang' || port.id === 'tanjung_pelepas') { |
| affected.push( |
| { code: 'CN', strength: 0.6, reason: 'Trade route dependency' }, |
| { code: 'JP', strength: 0.5, reason: 'Trade route' }, |
| { code: 'KR', strength: 0.5, reason: 'Trade route' }, |
| ); |
| } |
|
|
| |
| if (port.id === 'colon' || port.id === 'balboa') { |
| affected.push( |
| { code: 'US', strength: 0.5, reason: 'East-West coast shipping' }, |
| { code: 'CN', strength: 0.4, reason: 'Trade route to US East Coast' }, |
| ); |
| } |
|
|
| |
| if (port.id === 'aden' || port.id === 'djibouti' || port.id === 'hodeidah') { |
| affected.push( |
| { code: 'DE', strength: 0.5, reason: 'Europe-Asia shipping route' }, |
| { code: 'GB', strength: 0.5, reason: 'Shipping route' }, |
| { code: 'IT', strength: 0.4, reason: 'Mediterranean access' }, |
| { code: 'SA', strength: 0.4, reason: 'Regional trade' }, |
| ); |
| } |
|
|
| return affected; |
| } |
|
|
| function buildChokepointEdges( |
| graph: DependencyGraph, |
| waterways: WaterwayInput[], |
| ports: PortInput[], |
| ): void { |
| |
| for (const waterway of waterways) { |
| const chokepointId = `chokepoint:${waterway.id}`; |
|
|
| |
| const nearbyPorts = ports.filter(port => { |
| const dist = haversineKm(waterway.lat, waterway.lon, port.lat, port.lon); |
| return dist < 500; |
| }); |
|
|
| for (const port of nearbyPorts) { |
| addEdge(graph, { |
| from: chokepointId, |
| to: `port:${port.id}`, |
| type: 'controls_access', |
| strength: 0.7, |
| redundancy: 0.2, |
| metadata: { |
| relationship: 'Access controlled by chokepoint', |
| }, |
| }); |
| } |
|
|
| |
| const dependentCountries = getChokepointDependentCountries(waterway.id); |
| for (const dep of dependentCountries) { |
| const countryId = `country:${dep.code}`; |
| if (!graph.nodes.has(countryId)) { |
| graph.nodes.set(countryId, { |
| id: countryId, |
| type: 'country', |
| name: COUNTRY_NAMES[dep.code] || dep.code, |
| metadata: { code: dep.code }, |
| }); |
| } |
| addEdge(graph, { |
| from: chokepointId, |
| to: countryId, |
| type: 'trade_dependency', |
| strength: dep.strength, |
| redundancy: dep.redundancy, |
| metadata: { |
| relationship: dep.reason, |
| }, |
| }); |
| } |
| } |
| } |
|
|
| function getChokepointDependentCountries(chokepointId: string): { code: string; strength: number; redundancy: number; reason: string }[] { |
| |
| const dependencies: Record<string, { code: string; strength: number; redundancy: number; reason: string }[]> = { |
| 'suez': [ |
| { code: 'DE', strength: 0.6, redundancy: 0.3, reason: 'EU-Asia trade' }, |
| { code: 'IT', strength: 0.5, redundancy: 0.3, reason: 'Mediterranean' }, |
| { code: 'GB', strength: 0.5, redundancy: 0.4, reason: 'UK-Asia trade' }, |
| { code: 'CN', strength: 0.4, redundancy: 0.5, reason: 'China-EU exports' }, |
| ], |
| 'hormuz_strait': [ |
| { code: 'JP', strength: 0.8, redundancy: 0.2, reason: '80% oil imports' }, |
| { code: 'KR', strength: 0.7, redundancy: 0.2, reason: '70% oil imports' }, |
| { code: 'IN', strength: 0.6, redundancy: 0.3, reason: '60% oil imports' }, |
| { code: 'CN', strength: 0.5, redundancy: 0.4, reason: '40% oil imports' }, |
| ], |
| 'malacca_strait': [ |
| { code: 'CN', strength: 0.7, redundancy: 0.3, reason: '80% oil imports transit' }, |
| { code: 'JP', strength: 0.6, redundancy: 0.3, reason: 'Trade route' }, |
| { code: 'KR', strength: 0.6, redundancy: 0.3, reason: 'Trade route' }, |
| ], |
| 'bab_el_mandeb': [ |
| { code: 'DE', strength: 0.5, redundancy: 0.4, reason: 'EU shipping' }, |
| { code: 'GB', strength: 0.5, redundancy: 0.4, reason: 'UK shipping' }, |
| { code: 'SA', strength: 0.4, redundancy: 0.5, reason: 'Red Sea access' }, |
| ], |
| 'panama': [ |
| { code: 'US', strength: 0.5, redundancy: 0.4, reason: 'Inter-coast shipping' }, |
| { code: 'CN', strength: 0.4, redundancy: 0.5, reason: 'US East trade' }, |
| ], |
| 'gibraltar': [ |
| { code: 'ES', strength: 0.4, redundancy: 0.5, reason: 'Med access' }, |
| { code: 'IT', strength: 0.3, redundancy: 0.5, reason: 'Atlantic trade' }, |
| ], |
| 'bosphorus': [ |
| { code: 'RU', strength: 0.6, redundancy: 0.3, reason: 'Black Sea access' }, |
| { code: 'UA', strength: 0.6, redundancy: 0.3, reason: 'Grain exports' }, |
| { code: 'RO', strength: 0.4, redundancy: 0.4, reason: 'Black Sea trade' }, |
| ], |
| 'dardanelles': [ |
| { code: 'RU', strength: 0.5, redundancy: 0.3, reason: 'Black Sea access' }, |
| { code: 'UA', strength: 0.5, redundancy: 0.3, reason: 'Grain exports' }, |
| ], |
| 'taiwan_strait': [ |
| { code: 'TW', strength: 0.9, redundancy: 0.1, reason: 'Taiwan trade lifeline' }, |
| { code: 'JP', strength: 0.5, redundancy: 0.4, reason: 'Trade route' }, |
| { code: 'KR', strength: 0.4, redundancy: 0.4, reason: 'Trade route' }, |
| ], |
| }; |
| return dependencies[chokepointId] || []; |
| } |
|
|
| export function buildDependencyGraph(inputs: CascadeGraphInputs): DependencyGraph { |
| const cables = inputs.cables; |
| const pipelines = inputs.pipelines ?? PIPELINES; |
| const ports = inputs.ports ?? PORTS; |
| const waterways = inputs.waterways; |
|
|
| const graph: DependencyGraph = { |
| nodes: new Map(), |
| edges: [], |
| outgoing: new Map(), |
| incoming: new Map(), |
| cables, |
| }; |
|
|
| |
| addCablesAsNodes(graph, cables); |
| addPipelinesAsNodes(graph, pipelines); |
| addPortsAsNodes(graph, ports); |
| addChokepointsAsNodes(graph, waterways); |
| addCountriesAsNodes(graph, cables, pipelines); |
|
|
| |
| buildCableCountryEdges(graph, cables); |
| buildPipelineCountryEdges(graph, pipelines); |
| buildPortCountryEdges(graph, ports); |
| buildChokepointEdges(graph, waterways, ports); |
|
|
| return graph; |
| } |
|
|
| function categorizeImpact(strength: number): CascadeImpactLevel { |
| if (strength > 0.8) return 'critical'; |
| if (strength > 0.5) return 'high'; |
| if (strength > 0.2) return 'medium'; |
| return 'low'; |
| } |
|
|
| export function calculateCascade( |
| graph: DependencyGraph, |
| sourceId: string, |
| disruptionLevel: number = 1.0, |
| ): CascadeResult | null { |
| const source = graph.nodes.get(sourceId); |
|
|
| if (!source) return null; |
|
|
| const affected: Map<string, CascadeAffectedNode> = new Map(); |
| const visited = new Set<string>(); |
| visited.add(sourceId); |
|
|
| const queue: { nodeId: string; depth: number; path: string[] }[] = [ |
| { nodeId: sourceId, depth: 0, path: [sourceId] }, |
| ]; |
|
|
| while (queue.length > 0) { |
| const { nodeId, depth, path } = queue.shift()!; |
| if (depth >= 3) continue; |
|
|
| const dependents = graph.outgoing.get(nodeId) || []; |
|
|
| for (const edge of dependents) { |
| if (visited.has(edge.to)) continue; |
| visited.add(edge.to); |
|
|
| const impactStrength = edge.strength * disruptionLevel * (1 - (edge.redundancy || 0)); |
| const targetNode = graph.nodes.get(edge.to); |
|
|
| if (!targetNode || impactStrength < 0.05) continue; |
|
|
| affected.set(edge.to, { |
| node: targetNode, |
| impactLevel: categorizeImpact(impactStrength), |
| pathLength: depth + 1, |
| dependencyChain: [...path, edge.to], |
| redundancyAvailable: (edge.redundancy || 0) > 0.3, |
| estimatedRecovery: edge.metadata?.estimatedImpact, |
| }); |
|
|
| queue.push({ |
| nodeId: edge.to, |
| depth: depth + 1, |
| path: [...path, edge.to], |
| }); |
| } |
| } |
|
|
| const countriesAffected: CascadeCountryImpact[] = []; |
| for (const [nodeId, affectedNode] of affected) { |
| if (affectedNode.node.type === 'country') { |
| const code = (affectedNode.node.metadata?.code as string) || nodeId.replace('country:', ''); |
| countriesAffected.push({ |
| country: code, |
| countryName: affectedNode.node.name, |
| impactLevel: affectedNode.impactLevel, |
| affectedCapacity: getCapacityForCountry(sourceId, code, graph, affectedNode.dependencyChain), |
| }); |
| } |
| } |
|
|
| countriesAffected.sort((a, b) => { |
| const order = { critical: 0, high: 1, medium: 2, low: 3 }; |
| return (order[a.impactLevel] - order[b.impactLevel]) || (b.affectedCapacity - a.affectedCapacity); |
| }); |
|
|
| const redundancies = findRedundancies(graph, sourceId); |
|
|
| return { |
| source, |
| affectedNodes: Array.from(affected.values()), |
| countriesAffected, |
| redundancies, |
| }; |
| } |
|
|
| function getCapacityForCountry( |
| sourceId: string, |
| countryCode: string, |
| graph: DependencyGraph, |
| dependencyChain: string[], |
| ): number { |
| if (sourceId.startsWith('cable:')) { |
| const cableId = sourceId.replace('cable:', ''); |
| const cable = graph.cables.find(c => c.id === cableId); |
| const countryData = cable?.countriesServed?.find(cs => cs.country === countryCode); |
| return countryData?.capacityShare || 0; |
| } |
|
|
| |
| const countryId = `country:${countryCode}`; |
| const outgoing = graph.outgoing.get(sourceId) || []; |
| const direct = outgoing.filter(e => e.to === countryId); |
| if (direct.length > 0) { |
| const effective = direct.map(e => e.strength * (1 - (e.redundancy || 0))); |
| return Math.max(...effective); |
| } |
|
|
| |
| if (dependencyChain.length > 2) { |
| let pathCapacity = 1; |
| for (let i = 0; i < dependencyChain.length - 1; i++) { |
| const from = dependencyChain[i]!; |
| const to = dependencyChain[i + 1]!; |
| const stepEdges = graph.outgoing.get(from) || []; |
| const edge = stepEdges.find(e => e.to === to); |
| if (edge) { |
| pathCapacity *= edge.strength * (1 - (edge.redundancy || 0)); |
| } else { |
| pathCapacity = 0; |
| break; |
| } |
| } |
| if (pathCapacity > 0) return pathCapacity; |
| } |
|
|
| return 0; |
| } |
|
|
| function findRedundancies(graph: DependencyGraph, sourceId: string): CascadeResult['redundancies'] { |
| if (!sourceId.startsWith('cable:')) return []; |
|
|
| const cableId = sourceId.replace('cable:', ''); |
| const sourceCable = graph.cables.find(c => c.id === cableId); |
| if (!sourceCable) return []; |
|
|
| const sourceCountries = new Set(sourceCable.countriesServed?.map(c => c.country) || []); |
| const alternatives: NonNullable<CascadeResult['redundancies']> = []; |
|
|
| for (const cable of graph.cables) { |
| if (cable.id === cableId) continue; |
|
|
| const sharedCountries = cable.countriesServed?.filter(c => sourceCountries.has(c.country)) || []; |
| if (sharedCountries.length > 0) { |
| const avgCapacity = sharedCountries.reduce((sum, c) => sum + c.capacityShare, 0) / sharedCountries.length; |
| alternatives.push({ |
| id: cable.id, |
| name: cable.name, |
| capacityShare: avgCapacity, |
| }); |
| } |
| } |
|
|
| return alternatives.slice(0, 5); |
| } |
|
|
| export function getCableById(id: string, cables: CableInput[]): CableInput | undefined { |
| return cables.find(c => c.id === id); |
| } |
|
|
| export function getPipelineById(id: string, pipelines: PipelineInput[] = PIPELINES): PipelineInput | undefined { |
| return pipelines.find(p => p.id === id); |
| } |
|
|
| export function getPortById(id: string, ports: PortInput[] = PORTS): PortInput | undefined { |
| return ports.find(p => p.id === id); |
| } |
|
|
| export interface CascadeGraphStats { |
| nodes: number; |
| edges: number; |
| cables: number; |
| pipelines: number; |
| ports: number; |
| chokepoints: number; |
| countries: number; |
| } |
|
|
| export function getGraphStats(graph: DependencyGraph): CascadeGraphStats { |
| let cables = 0, pipelines = 0, ports = 0, chokepoints = 0, countries = 0; |
|
|
| for (const node of graph.nodes.values()) { |
| if (node.type === 'cable') cables++; |
| else if (node.type === 'pipeline') pipelines++; |
| else if (node.type === 'port') ports++; |
| else if (node.type === 'chokepoint') chokepoints++; |
| else if (node.type === 'country') countries++; |
| } |
|
|
| return { |
| nodes: graph.nodes.size, |
| edges: graph.edges.length, |
| cables, |
| pipelines, |
| ports, |
| chokepoints, |
| countries, |
| }; |
| } |
|
|