import { ChangeDetectionStrategy, Component, computed, inject, input, signal } from '@angular/core'; import { Router, RouterLink } from '@angular/router'; import { SIGNATURE_COPY } from '../../core/content/signature-copy'; import { type AppLocale } from '../../core/i18n/locale'; import { LocaleService } from '../../core/i18n/locale.service'; import { NavigationService } from '../../core/navigation/navigation.service'; import { routePath } from '../../core/routing/route-paths'; import { connectedNodeIds, SYSTEMS_MAP_EDGES, SYSTEMS_MAP_NODE_KINDS, SYSTEMS_MAP_NODES, type SystemsMapNode, type SystemsMapNodeId, } from './systems-map.model'; let systemsMapInstanceId = 0; export interface SystemsMapNodeView { readonly id: SystemsMapNodeId; readonly kind: SystemsMapNode['kind']; readonly routeId: SystemsMapNode['routeId']; readonly x: number; readonly y: number; readonly label: string; readonly summary: string; readonly href: string; readonly link: unknown[]; readonly relationship: string; readonly accessibleName: string; } export interface SystemsMapEdgeView { readonly from: SystemsMapNodeId; readonly to: SystemsMapNodeId; readonly x1: number; readonly y1: number; readonly x2: number; readonly y2: number; } @Component({ selector: 'app-systems-map', changeDetection: ChangeDetectionStrategy.OnPush, imports: [RouterLink], templateUrl: './systems-map.html', styleUrl: './systems-map.scss', }) export class SystemsMap { readonly heading = input(''); readonly intro = input(''); readonly headingLevel = input<2 | 3>(2); private readonly navigation = inject(NavigationService); private readonly localeService = inject(LocaleService); private readonly router = inject(Router); private readonly instanceId = systemsMapInstanceId++; protected readonly headingId = `systems-map-heading-${this.instanceId}`; protected readonly svgTitleId = `systems-map-svg-title-${this.instanceId}`; protected readonly svgDescId = `systems-map-svg-desc-${this.instanceId}`; protected readonly listHeadingId = `systems-map-list-${this.instanceId}`; protected readonly legendHeadingId = `systems-map-legend-${this.instanceId}`; protected readonly activeNodeId = signal(null); protected readonly copy = computed(() => SIGNATURE_COPY[this.localeService.locale()].systemsMap); protected readonly headingText = computed(() => { const override = this.heading().trim(); return override.length > 0 ? override : this.copy().heading; }); protected readonly introText = computed(() => { const override = this.intro().trim(); return override.length > 0 ? override : this.copy().intro; }); protected readonly nodes = computed(() => { const locale = this.localeService.locale(); const copy = this.copy(); return SYSTEMS_MAP_NODES.map((node) => this.toNodeView(node, locale, copy.relationshipLabel)); }); protected readonly edges = computed((): readonly SystemsMapEdgeView[] => { const byId = new Map(SYSTEMS_MAP_NODES.map((node) => [node.id, node])); return SYSTEMS_MAP_EDGES.map((edge) => { const from = byId.get(edge.from); const to = byId.get(edge.to); return { from: edge.from, to: edge.to, x1: from?.x ?? 0, y1: from?.y ?? 0, x2: to?.x ?? 0, y2: to?.y ?? 0, }; }); }); protected readonly legendItems = computed(() => { const legend = this.copy().legend; return SYSTEMS_MAP_NODE_KINDS.map((kind) => ({ kind, label: legend[kind], })); }); protected readonly activeReadout = computed(() => { const activeId = this.activeNodeId(); if (!activeId) { return ''; } const node = this.nodes().find((entry) => entry.id === activeId); return node ? `${node.label}: ${node.relationship}` : ''; }); protected onNodeActivate(event: MouseEvent, node: SystemsMapNodeView): void { if (event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) { return; } event.preventDefault(); void this.router.navigate(this.navigation.link(node.routeId)); } protected onNodeEnter(id: SystemsMapNodeId): void { this.activeNodeId.set(id); } protected onNodeLeave(id: SystemsMapNodeId): void { if (this.activeNodeId() === id) { this.activeNodeId.set(null); } } protected isConnectedNode(id: SystemsMapNodeId): boolean { const active = this.activeNodeId(); if (!active) { return false; } return active === id || this.neighborSet(active).has(id); } protected isDimmedNode(id: SystemsMapNodeId): boolean { const active = this.activeNodeId(); if (!active) { return false; } return active !== id && !this.neighborSet(active).has(id); } protected isConnectedEdge(edge: SystemsMapEdgeView): boolean { const active = this.activeNodeId(); if (!active) { return false; } return edge.from === active || edge.to === active; } protected isDimmedEdge(edge: SystemsMapEdgeView): boolean { const active = this.activeNodeId(); if (!active) { return false; } return edge.from !== active && edge.to !== active; } private toNodeView( node: SystemsMapNode, locale: AppLocale, relationshipLabel: string, ): SystemsMapNodeView { const connected = new Set(connectedNodeIds(node.id)); const connectedLabels = SYSTEMS_MAP_NODES.filter((entry) => connected.has(entry.id)).map( (entry) => entry.label[locale], ); const relationship = relationshipLabel.replace('{targets}', connectedLabels.join(', ')); const label = node.label[locale]; const summary = node.summary[locale]; return { id: node.id, kind: node.kind, routeId: node.routeId, x: node.x, y: node.y, label, summary, href: routePath(node.routeId, locale), link: this.navigation.link(node.routeId), relationship, accessibleName: `${label}. ${summary} ${relationship}`, }; } private neighborSet(id: SystemsMapNodeId): ReadonlySet { return new Set(connectedNodeIds(id)); } }