// src/features/imageAnnotation/services/AnnotationManager.ts // 全局注释管理器 - 负责监听Cornerstone事件并管理注释的保存/加载 import { eventTarget, getRenderingEngines } from '@cornerstonejs/core'; import { EVENTS } from '@cornerstonejs/core'; import { annotation as corAnnotation, Enums } from '@cornerstonejs/tools'; import { AnnotationSerializer } from './AnnotationSerializer'; import { AnnotationValidator } from './AnnotationValidator'; import { annotationAPI } from '../../../API/annotation'; import { extractSopInstanceUid } from '../../../utils/dicomUtils'; import type { AnnotationData, AnnotationEventType } from '../types/annotation'; import { IRenderingEngine, IStackViewport } from '@cornerstonejs/core/dist/esm/types'; import * as cornerstoneTools from '@cornerstonejs/tools'; import { getIpPort } from '../../../API/config'; export class AnnotationManager { private eventListenersRegistered: boolean = false; private activeImageId: string | null = null; private serializer: AnnotationSerializer; private validator: AnnotationValidator; private pendingChanges: Map = new Map(); private saveTimeout: NodeJS.Timeout | null = null; private isLoading: boolean = false; constructor() { this.serializer = new AnnotationSerializer(); this.validator = new AnnotationValidator(); } async initialize(): Promise { console.log('【annotationmanager】🔧 AnnotationManager 初始化中...'); // 注册全局事件监听器 this.registerGlobalEventListeners(); console.log('【annotationmanager】🎧 全局注释事件监听器已设置'); } async cleanup(): Promise { console.log('【annotationmanager】🧹 AnnotationManager 清理中...'); // 取消待处理的保存操作 if (this.saveTimeout) { clearTimeout(this.saveTimeout); this.saveTimeout = null; } // 移除全局事件监听器 this.unregisterGlobalEventListeners(); console.log('【annotationmanager】✅ AnnotationManager 清理完成'); } // ============ 事件监听器注册 ============ private registerGlobalEventListeners(): void { if (this.eventListenersRegistered) { console.warn('【annotationmanager】事件监听器已注册,跳过重复注册'); return; } console.log('【annotationmanager】注册事件监听器...'); // 图像加载完成事件 eventTarget.addEventListener( EVENTS.IMAGE_LOADED, this.handleImageLoadCompleted ); // 图像渲染完成事件 eventTarget.addEventListener( EVENTS.IMAGE_RENDERED, this.handleImageRendered ); // 注释完成事件 eventTarget.addEventListener( Enums.Events.ANNOTATION_COMPLETED, this.handleAnnotationCompleted ); // 注释添加事件 eventTarget.addEventListener( Enums.Events.ANNOTATION_ADDED, this.handleAnnotationAdded ); // 注释修改事件 eventTarget.addEventListener( Enums.Events.ANNOTATION_MODIFIED, this.handleAnnotationModified ); // 注释删除事件 eventTarget.addEventListener( Enums.Events.ANNOTATION_REMOVED, this.handleAnnotationRemoved ); this.eventListenersRegistered = true; console.log('【annotationmanager】✅ 事件监听器注册完成'); } private unregisterGlobalEventListeners(): void { if (!this.eventListenersRegistered) { return; } console.log('【annotationmanager】移除事件监听器...'); eventTarget.removeEventListener( EVENTS.IMAGE_LOADED, this.handleImageLoadCompleted ); eventTarget.removeEventListener( EVENTS.IMAGE_RENDERED, this.handleImageRendered ); eventTarget.removeEventListener( Enums.Events.ANNOTATION_COMPLETED, this.handleAnnotationCompleted ); eventTarget.removeEventListener( Enums.Events.ANNOTATION_ADDED, this.handleAnnotationCompleted ); eventTarget.removeEventListener( Enums.Events.ANNOTATION_MODIFIED, this.handleAnnotationModified ); eventTarget.removeEventListener( Enums.Events.ANNOTATION_REMOVED, this.handleAnnotationRemoved ); this.eventListenersRegistered = false; console.log('【annotationmanager】✅ 事件监听器移除完成'); } // ============ 事件处理方法 ============ private handleImageLoadCompleted = (evt: any) => { const rawImageId = evt.detail.image?.imageId || evt.detail.imageId; console.log(`【annotationmanager】📸 原始图像ID: ${rawImageId}`); // 提取SOP Instance UID const sopInstanceUid = extractSopInstanceUid(rawImageId); if (!sopInstanceUid) { console.warn('【annotationmanager】无法从图像URL中提取SOP Instance UID:', rawImageId); return; } console.log(`【annotationmanager】📸 提取的SOP Instance UID: ${sopInstanceUid}`); // 使用提取的SOP Instance UID加载注释 this.loadAnnotationsForImage(sopInstanceUid); }; private handleImageRendered = (evt: any) => { const { viewportId } = evt.detail; console.log(`【annotationmanager】🎨 图像渲染完成: ${viewportId}`); // TODO: 确保注释正确显示 }; private handleAnnotationCompleted = (evt: any) => { console.log('🎯 监听到注释完成事件:', evt); console.log('📋 事件详情:', evt.detail); const { annotation, toolName } = evt.detail; console.log(`【annotationmanager】✏️ 注释创建完成: ${toolName}`); // 确保有活动的图像ID if (!this.activeImageId) { console.warn('【annotationmanager】没有活动的图像ID,跳过注释保存'); return; } // 实现注释保存逻辑 this.saveAnnotation(annotation, toolName); }; private handleAnnotationAdded = (evt: any) => { console.log('【annotationmanager】🎯 监听到注释添加事件:', evt); console.log('【annotationmanager】📋 事件详情:', evt.detail); const { annotation } = evt.detail; const toolName = annotation.metadata?.toolName; console.log(`【annotationmanager】➕ 注释添加: ${toolName}`); // 确保有活动的图像ID if (!this.activeImageId) { console.warn('【annotationmanager】没有活动的图像ID,跳过注释保存'); return; } // 实现注释保存逻辑 this.saveAnnotation(annotation, toolName); }; private handleAnnotationModified = (evt: any) => { console.log('🎯 监听到注释修改事件:', evt); console.log('📋 事件详情:', evt.detail); const { annotation, toolName } = evt.detail; console.log(`【annotationmanager】🔄 注释修改完成: ${toolName}`); // 确保有活动的图像ID if (!this.activeImageId) { console.warn('【annotationmanager】没有活动的图像ID,跳过注释更新'); return; } // 实现注释更新逻辑 this.updateAnnotation(annotation, toolName); }; private handleAnnotationRemoved = (evt: any) => { console.log('🎯 监听到注释删除事件:', evt); console.log('📋 事件详情:', evt.detail); const { annotation } = evt.detail; const annotationUID = annotation.annotationUID; console.log(`【annotationmanager】🗑️ 注释删除完成: ${annotationUID}`); // 确保有活动的图像ID if (!this.activeImageId) { console.warn('【annotationmanager】没有活动的图像ID,跳过注释删除'); return; } // 实现注释删除逻辑 this.deleteAnnotation(annotation); }; // ============ 注释操作方法 ============ private async loadAnnotationsForImage(imageId: string): Promise { if (this.isLoading) { console.log(`【annotationmanager】注释正在加载中,跳过: ${imageId}`); return; } this.isLoading = true; this.activeImageId = imageId; try { console.log(`【annotationmanager】📥 加载注释数据: ${imageId}`); // 调用API获取注释数据 const response = await annotationAPI.getAnnotations(imageId); if (response.data && Array.isArray(response.data)) { const annotationStrings = response.data; // 验证注释数据 // this.validator.validateAnnotationArray(annotationStrings); // 反序列化并渲染注释 await this.renderAnnotations(annotationStrings); console.log(`【annotationmanager】✅ 注释加载完成: ${imageId} (${annotationStrings.length} 个注释)`); } else { console.log(`【annotationmanager】ℹ️ 图像 ${imageId} 没有注释数据`); } } catch (error) { console.error(`【annotationmanager】❌ 注释加载失败 ${imageId}:`, error); // 不抛出错误,让应用继续运行 } finally { this.isLoading = false; } } private async saveAnnotation(annotation: any, toolName: string): Promise { try { console.log(`【annotationmanager】💾 保存注释: ${toolName}`); // 序列化注释数据 const serializedData = this.serializer.serialize(annotation, toolName); // 验证序列化数据 // this.validator.validateAnnotationData(serializedData); // 添加到待保存队列(延迟保存) this.scheduleSave(serializedData); console.log(`【annotationmanager】✅ 注释保存任务已安排: ${toolName}`); } catch (error) { console.error(`【annotationmanager】❌ 注释保存失败 ${toolName}:`, error); } } private async updateAnnotation(annotation: any, toolName: string): Promise { try { console.log(`【annotationmanager】🔄 更新注释: ${toolName}`); // 复用保存逻辑 await this.saveAnnotation(annotation, toolName); console.log(`【annotationmanager】✅ 注释更新完成: ${toolName}`); } catch (error) { console.error(`【annotationmanager】❌ 注释更新失败 ${toolName}:`, error); } } private async deleteAnnotation(annotation: any): Promise { try { const annotationUID = annotation.annotationUID; console.log(`【annotationmanager】🗑️ 删除注释: ${annotationUID}`); if (!this.activeImageId) { console.warn('【annotationmanager】没有活动的图像ID,跳过删除操作'); return; } // 从 annotation 中获取 FrameOfReferenceUID const forUID = annotation.metadata?.referencedImageId; if (!forUID) { console.warn('【annotationmanager】FrameOfReferenceUID 未找到,跳过删除操作'); return; } // 用 FrameOfReferenceUID 查找相关的 elements const elements = this.findElementsByFOR(forUID); // 收集删除后的所有注释 const allAnnotationStrings: string[] = []; elements.forEach(element => { const anns = corAnnotation.state.getAnnotations('', element); if (anns) { // 序列化所有注释 for (const [toolName, ann] of Object.entries(anns as any)) { try { const serialized = this.serializer.serialize(ann, toolName); allAnnotationStrings.push(serialized); } catch (error) { console.error('序列化注释失败:', error); // 跳过序列化失败的注释 } } } }); // 保存所有注释 await annotationAPI.saveAnnotations(this.activeImageId, allAnnotationStrings); console.log(`【annotationmanager】✅ 注释删除完成: ${annotationUID} (剩余 ${allAnnotationStrings.length} 个注释)`); } catch (error) { console.error(`【annotationmanager】❌ 注释删除失败:`, error); } } /** * 通过 SOP Instance UID 检查 viewport 是否包含指定图像 * @param viewport Stack viewport 实例 * @param imageId 要检查的图像ID * @returns 是否包含该图像 */ private hasImageBySopInstanceUid(viewport: IStackViewport, imageId: string): boolean { try { // 1. 提取传入 imageId 的 SOP Instance UID const targetSopUid = extractSopInstanceUid(imageId); if (!targetSopUid) { console.warn('【annotationmanager】无法提取目标图像的 SOP Instance UID:', imageId); return false; } // 2. 获取 viewport 中的所有 imageIds const imageIds = viewport.getImageIds?.(); if (!imageIds || imageIds.length === 0) { return false; } // 3. 遍历比较每个 imageId 的 SOP Instance UID for (const vpImageId of imageIds) { const vpSopUid = extractSopInstanceUid(vpImageId); if (vpSopUid && vpSopUid === targetSopUid) { return true; } } return false; } catch (error) { console.error('【annotationmanager】比较 SOP Instance UID 时出错:', error); return false; } } // 用 FrameOfReferenceUID 查找 viewport elements private findElementsByFOR(imageId: string): any[] { const elements: any[] = []; const renderingEngines = getRenderingEngines(); if (renderingEngines) { renderingEngines.forEach((re: IRenderingEngine) => { re.getViewports().forEach((vp: IStackViewport) => { if (vp && vp.element) { const stackVp = vp as IStackViewport; if (this.hasImageBySopInstanceUid(stackVp, imageId)) { elements.push(vp.element); } } }); }); } else { console.warn('【annotationmanager】getRenderingEngines 方法不可用,无法查找元素'); } return elements; } /** * 更新注释对象中的 URL,替换为当前服务器地址 * @param annotation 注释对象 * @returns 更新后的注释对象 */ private updateAnnotationUrls(annotation: any): any { try { const currentIpPort = getIpPort(); if (!currentIpPort) { console.warn('【annotationmanager】无法获取当前服务器地址,跳过 URL 更新'); return annotation; } // 正则表达式:匹配 URL 中的协议+域名/IP+端口部分 // 例如:dicomweb:http://192.168.110.245:6001/path -> 提取 http://192.168.110.245:6001 const urlRegex = /(https?:\/\/[^\/]+)/; // 更新 metadata.referencedImageId if (annotation.metadata?.referencedImageId) { const originalUrl = annotation.metadata.referencedImageId; const updatedUrl = originalUrl.replace(urlRegex, currentIpPort); annotation.metadata.referencedImageId = updatedUrl; console.log(`【annotationmanager】已更新 referencedImageId: ${originalUrl} -> ${updatedUrl}`); } // 更新 metadata.referencedImageURI(如果存在) if (annotation.metadata?.referencedImageURI) { const originalUrl = annotation.metadata.referencedImageURI; const updatedUrl = originalUrl.replace(urlRegex, currentIpPort); annotation.metadata.referencedImageURI = updatedUrl; console.log(`【annotationmanager】已更新 referencedImageURI: ${originalUrl} -> ${updatedUrl}`); } return annotation; } catch (error) { console.error('【annotationmanager】更新注释 URL 失败:', error); return annotation; } } private async renderAnnotations(annotationStrings: string[]): Promise { console.log(`【annotationmanager】🎨 渲染 ${annotationStrings.length} 个注释`); for (const annotationString of annotationStrings) { try { // 反序列化为Cornerstone格式 let deserializedAnnotation = this.serializer.deserialize(annotationString); // 更新注释中的 URL,替换为当前服务器地址 deserializedAnnotation = this.updateAnnotationUrls(deserializedAnnotation); // 1. 从注释元数据中获取图像ID const imageId = deserializedAnnotation.metadata?.referencedImageId; if (!imageId) { console.warn('【annotationmanager】注释缺少 referencedImageId,跳过'); continue; } // 2. 找到包含该图像的 viewport element const renderingEngines = getRenderingEngines(); let targetElement: HTMLDivElement | null = null; if (renderingEngines) { for (const engine of renderingEngines) { const viewports = engine.getViewports(); for (const viewport of viewports) { // 使用类型断言处理 IStackViewport const stackViewport = viewport as IStackViewport; if (stackViewport) { if (this.hasImageBySopInstanceUid(stackViewport, imageId)) { targetElement = stackViewport.element; break; } } } if (targetElement) break; } } if (!targetElement) { console.warn('【annotationmanager】未找到对应的 viewport,跳过'); continue; } console.log(`【annotationmanager】渲染注释 ${JSON.stringify(deserializedAnnotation)} 到 imageid ${imageId} 的 viewport`); // 3. ✅ 使用正确的 element 参数添加注释 corAnnotation.state.addAnnotation(deserializedAnnotation, targetElement); console.log(`【annotationmanager】✅ 注释渲染成功到 viewport`); } catch (error) { console.error(`【annotationmanager】❌ 注释渲染失败:`, error); // 继续处理其他注释 } } } // ============ 延迟保存机制 ============ /** * 安排延迟保存 */ private scheduleSave(annotationString: string): void { try { // 从JSON字符串中提取ID作为key const annotation = JSON.parse(annotationString); const key = annotation.annotationUID || annotation.id || `temp_${Date.now()}`; // 添加到待保存队列 this.pendingChanges.set(key, annotationString); // 取消之前的延迟保存 if (this.saveTimeout) { clearTimeout(this.saveTimeout); } // 设置新的延迟保存(1秒后) this.saveTimeout = setTimeout(() => { this.performBulkSave(); }, 1000); } catch (error) { console.error('安排保存时解析注释失败:', error); } } /** * 执行批量保存 */ private async performBulkSave(): Promise { if (this.pendingChanges.size === 0 || !this.activeImageId) { return; } const changes = Array.from(this.pendingChanges.values()); this.pendingChanges.clear(); try { console.log(`【annotationmanager】💾 批量保存 ${changes.length} 个注释`); // 调用API保存注释 await annotationAPI.saveAnnotations(this.activeImageId, changes); console.log(`【annotationmanager】✅ 批量保存成功: ${changes.length} 个注释`); } catch (error) { console.error(`【annotationmanager】❌ 批量保存失败:`, error); // 保存失败时,重新加入待保存队列 changes.forEach(change => { try { const annotation = JSON.parse(change); const key = annotation.annotationUID || annotation.id || `temp_${Date.now()}`; this.pendingChanges.set(key, change); } catch (error) { console.error('重新加入待保存队列时解析失败:', error); } }); // 可以在这里实现重试逻辑 } } }