AnnotationManager.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579
  1. // src/features/imageAnnotation/services/AnnotationManager.ts
  2. // 全局注释管理器 - 负责监听Cornerstone事件并管理注释的保存/加载
  3. import { eventTarget, getRenderingEngines } from '@cornerstonejs/core';
  4. import { EVENTS } from '@cornerstonejs/core';
  5. import { annotation as corAnnotation, Enums } from '@cornerstonejs/tools';
  6. import { AnnotationSerializer } from './AnnotationSerializer';
  7. import { AnnotationValidator } from './AnnotationValidator';
  8. import { annotationAPI } from '../../../API/annotation';
  9. import { extractSopInstanceUid } from '../../../utils/dicomUtils';
  10. import type { AnnotationData, AnnotationEventType } from '../types/annotation';
  11. import { IRenderingEngine, IStackViewport } from '@cornerstonejs/core/dist/esm/types';
  12. import * as cornerstoneTools from '@cornerstonejs/tools';
  13. import { getIpPort } from '../../../API/config';
  14. export class AnnotationManager {
  15. private eventListenersRegistered: boolean = false;
  16. private activeImageId: string | null = null;
  17. private serializer: AnnotationSerializer;
  18. private validator: AnnotationValidator;
  19. private pendingChanges: Map<string, string> = new Map();
  20. private saveTimeout: NodeJS.Timeout | null = null;
  21. private isLoading: boolean = false;
  22. constructor() {
  23. this.serializer = new AnnotationSerializer();
  24. this.validator = new AnnotationValidator();
  25. }
  26. async initialize(): Promise<void> {
  27. console.log('【annotationmanager】🔧 AnnotationManager 初始化中...');
  28. // 注册全局事件监听器
  29. this.registerGlobalEventListeners();
  30. console.log('【annotationmanager】🎧 全局注释事件监听器已设置');
  31. }
  32. async cleanup(): Promise<void> {
  33. console.log('【annotationmanager】🧹 AnnotationManager 清理中...');
  34. // 取消待处理的保存操作
  35. if (this.saveTimeout) {
  36. clearTimeout(this.saveTimeout);
  37. this.saveTimeout = null;
  38. }
  39. // 移除全局事件监听器
  40. this.unregisterGlobalEventListeners();
  41. console.log('【annotationmanager】✅ AnnotationManager 清理完成');
  42. }
  43. // ============ 事件监听器注册 ============
  44. private registerGlobalEventListeners(): void {
  45. if (this.eventListenersRegistered) {
  46. console.warn('【annotationmanager】事件监听器已注册,跳过重复注册');
  47. return;
  48. }
  49. console.log('【annotationmanager】注册事件监听器...');
  50. // 图像加载完成事件
  51. eventTarget.addEventListener(
  52. EVENTS.IMAGE_LOADED,
  53. this.handleImageLoadCompleted
  54. );
  55. // 图像渲染完成事件
  56. eventTarget.addEventListener(
  57. EVENTS.IMAGE_RENDERED,
  58. this.handleImageRendered
  59. );
  60. // 注释完成事件
  61. eventTarget.addEventListener(
  62. Enums.Events.ANNOTATION_COMPLETED,
  63. this.handleAnnotationCompleted
  64. );
  65. // 注释添加事件
  66. eventTarget.addEventListener(
  67. Enums.Events.ANNOTATION_ADDED,
  68. this.handleAnnotationAdded
  69. );
  70. // 注释修改事件
  71. eventTarget.addEventListener(
  72. Enums.Events.ANNOTATION_MODIFIED,
  73. this.handleAnnotationModified
  74. );
  75. // 注释删除事件
  76. eventTarget.addEventListener(
  77. Enums.Events.ANNOTATION_REMOVED,
  78. this.handleAnnotationRemoved
  79. );
  80. this.eventListenersRegistered = true;
  81. console.log('【annotationmanager】✅ 事件监听器注册完成');
  82. }
  83. private unregisterGlobalEventListeners(): void {
  84. if (!this.eventListenersRegistered) {
  85. return;
  86. }
  87. console.log('【annotationmanager】移除事件监听器...');
  88. eventTarget.removeEventListener(
  89. EVENTS.IMAGE_LOADED,
  90. this.handleImageLoadCompleted
  91. );
  92. eventTarget.removeEventListener(
  93. EVENTS.IMAGE_RENDERED,
  94. this.handleImageRendered
  95. );
  96. eventTarget.removeEventListener(
  97. Enums.Events.ANNOTATION_COMPLETED,
  98. this.handleAnnotationCompleted
  99. );
  100. eventTarget.removeEventListener(
  101. Enums.Events.ANNOTATION_ADDED,
  102. this.handleAnnotationCompleted
  103. );
  104. eventTarget.removeEventListener(
  105. Enums.Events.ANNOTATION_MODIFIED,
  106. this.handleAnnotationModified
  107. );
  108. eventTarget.removeEventListener(
  109. Enums.Events.ANNOTATION_REMOVED,
  110. this.handleAnnotationRemoved
  111. );
  112. this.eventListenersRegistered = false;
  113. console.log('【annotationmanager】✅ 事件监听器移除完成');
  114. }
  115. // ============ 事件处理方法 ============
  116. private handleImageLoadCompleted = (evt: any) => {
  117. const rawImageId = evt.detail.image?.imageId || evt.detail.imageId;
  118. console.log(`【annotationmanager】📸 原始图像ID: ${rawImageId}`);
  119. // 提取SOP Instance UID
  120. const sopInstanceUid = extractSopInstanceUid(rawImageId);
  121. if (!sopInstanceUid) {
  122. console.warn('【annotationmanager】无法从图像URL中提取SOP Instance UID:', rawImageId);
  123. return;
  124. }
  125. console.log(`【annotationmanager】📸 提取的SOP Instance UID: ${sopInstanceUid}`);
  126. // 使用提取的SOP Instance UID加载注释
  127. this.loadAnnotationsForImage(sopInstanceUid);
  128. };
  129. private handleImageRendered = (evt: any) => {
  130. const { viewportId } = evt.detail;
  131. console.log(`【annotationmanager】🎨 图像渲染完成: ${viewportId}`);
  132. // TODO: 确保注释正确显示
  133. };
  134. private handleAnnotationCompleted = (evt: any) => {
  135. console.log('🎯 监听到注释完成事件:', evt);
  136. console.log('📋 事件详情:', evt.detail);
  137. const { annotation, toolName } = evt.detail;
  138. console.log(`【annotationmanager】✏️ 注释创建完成: ${toolName}`);
  139. // 确保有活动的图像ID
  140. if (!this.activeImageId) {
  141. console.warn('【annotationmanager】没有活动的图像ID,跳过注释保存');
  142. return;
  143. }
  144. // 实现注释保存逻辑
  145. this.saveAnnotation(annotation, toolName);
  146. };
  147. private handleAnnotationAdded = (evt: any) => {
  148. console.log('【annotationmanager】🎯 监听到注释添加事件:', evt);
  149. console.log('【annotationmanager】📋 事件详情:', evt.detail);
  150. const { annotation } = evt.detail;
  151. const toolName = annotation.metadata?.toolName;
  152. console.log(`【annotationmanager】➕ 注释添加: ${toolName}`);
  153. // 确保有活动的图像ID
  154. if (!this.activeImageId) {
  155. console.warn('【annotationmanager】没有活动的图像ID,跳过注释保存');
  156. return;
  157. }
  158. // 实现注释保存逻辑
  159. this.saveAnnotation(annotation, toolName);
  160. };
  161. private handleAnnotationModified = (evt: any) => {
  162. console.log('🎯 监听到注释修改事件:', evt);
  163. console.log('📋 事件详情:', evt.detail);
  164. const { annotation, toolName } = evt.detail;
  165. console.log(`【annotationmanager】🔄 注释修改完成: ${toolName}`);
  166. // 确保有活动的图像ID
  167. if (!this.activeImageId) {
  168. console.warn('【annotationmanager】没有活动的图像ID,跳过注释更新');
  169. return;
  170. }
  171. // 实现注释更新逻辑
  172. this.updateAnnotation(annotation, toolName);
  173. };
  174. private handleAnnotationRemoved = (evt: any) => {
  175. console.log('🎯 监听到注释删除事件:', evt);
  176. console.log('📋 事件详情:', evt.detail);
  177. const { annotation } = evt.detail;
  178. const annotationUID = annotation.annotationUID;
  179. console.log(`【annotationmanager】🗑️ 注释删除完成: ${annotationUID}`);
  180. // 确保有活动的图像ID
  181. if (!this.activeImageId) {
  182. console.warn('【annotationmanager】没有活动的图像ID,跳过注释删除');
  183. return;
  184. }
  185. // 实现注释删除逻辑
  186. this.deleteAnnotation(annotation);
  187. };
  188. // ============ 注释操作方法 ============
  189. private async loadAnnotationsForImage(imageId: string): Promise<void> {
  190. if (this.isLoading) {
  191. console.log(`【annotationmanager】注释正在加载中,跳过: ${imageId}`);
  192. return;
  193. }
  194. this.isLoading = true;
  195. this.activeImageId = imageId;
  196. try {
  197. console.log(`【annotationmanager】📥 加载注释数据: ${imageId}`);
  198. // 调用API获取注释数据
  199. const response = await annotationAPI.getAnnotations(imageId);
  200. if (response.data && Array.isArray(response.data)) {
  201. const annotationStrings = response.data;
  202. // 验证注释数据
  203. // this.validator.validateAnnotationArray(annotationStrings);
  204. // 反序列化并渲染注释
  205. await this.renderAnnotations(annotationStrings);
  206. console.log(`【annotationmanager】✅ 注释加载完成: ${imageId} (${annotationStrings.length} 个注释)`);
  207. } else {
  208. console.log(`【annotationmanager】ℹ️ 图像 ${imageId} 没有注释数据`);
  209. }
  210. } catch (error) {
  211. console.error(`【annotationmanager】❌ 注释加载失败 ${imageId}:`, error);
  212. // 不抛出错误,让应用继续运行
  213. } finally {
  214. this.isLoading = false;
  215. }
  216. }
  217. private async saveAnnotation(annotation: any, toolName: string): Promise<void> {
  218. try {
  219. console.log(`【annotationmanager】💾 保存注释: ${toolName}`);
  220. // 序列化注释数据
  221. const serializedData = this.serializer.serialize(annotation, toolName);
  222. // 验证序列化数据
  223. // this.validator.validateAnnotationData(serializedData);
  224. // 添加到待保存队列(延迟保存)
  225. this.scheduleSave(serializedData);
  226. console.log(`【annotationmanager】✅ 注释保存任务已安排: ${toolName}`);
  227. } catch (error) {
  228. console.error(`【annotationmanager】❌ 注释保存失败 ${toolName}:`, error);
  229. }
  230. }
  231. private async updateAnnotation(annotation: any, toolName: string): Promise<void> {
  232. try {
  233. console.log(`【annotationmanager】🔄 更新注释: ${toolName}`);
  234. // 复用保存逻辑
  235. await this.saveAnnotation(annotation, toolName);
  236. console.log(`【annotationmanager】✅ 注释更新完成: ${toolName}`);
  237. } catch (error) {
  238. console.error(`【annotationmanager】❌ 注释更新失败 ${toolName}:`, error);
  239. }
  240. }
  241. private async deleteAnnotation(annotation: any): Promise<void> {
  242. try {
  243. const annotationUID = annotation.annotationUID;
  244. console.log(`【annotationmanager】🗑️ 删除注释: ${annotationUID}`);
  245. if (!this.activeImageId) {
  246. console.warn('【annotationmanager】没有活动的图像ID,跳过删除操作');
  247. return;
  248. }
  249. // 从 annotation 中获取 FrameOfReferenceUID
  250. const forUID = annotation.metadata?.referencedImageId;
  251. if (!forUID) {
  252. console.warn('【annotationmanager】FrameOfReferenceUID 未找到,跳过删除操作');
  253. return;
  254. }
  255. // 用 FrameOfReferenceUID 查找相关的 elements
  256. const elements = this.findElementsByFOR(forUID);
  257. // 收集删除后的所有注释
  258. const allAnnotationStrings: string[] = [];
  259. elements.forEach(element => {
  260. const anns = corAnnotation.state.getAnnotations('', element);
  261. if (anns) {
  262. // 序列化所有注释
  263. for (const [toolName, ann] of Object.entries(anns as any)) {
  264. try {
  265. const serialized = this.serializer.serialize(ann, toolName);
  266. allAnnotationStrings.push(serialized);
  267. } catch (error) {
  268. console.error('序列化注释失败:', error);
  269. // 跳过序列化失败的注释
  270. }
  271. }
  272. }
  273. });
  274. // 保存所有注释
  275. await annotationAPI.saveAnnotations(this.activeImageId, allAnnotationStrings);
  276. console.log(`【annotationmanager】✅ 注释删除完成: ${annotationUID} (剩余 ${allAnnotationStrings.length} 个注释)`);
  277. } catch (error) {
  278. console.error(`【annotationmanager】❌ 注释删除失败:`, error);
  279. }
  280. }
  281. /**
  282. * 通过 SOP Instance UID 检查 viewport 是否包含指定图像
  283. * @param viewport Stack viewport 实例
  284. * @param imageId 要检查的图像ID
  285. * @returns 是否包含该图像
  286. */
  287. private hasImageBySopInstanceUid(viewport: IStackViewport, imageId: string): boolean {
  288. try {
  289. // 1. 提取传入 imageId 的 SOP Instance UID
  290. const targetSopUid = extractSopInstanceUid(imageId);
  291. if (!targetSopUid) {
  292. console.warn('【annotationmanager】无法提取目标图像的 SOP Instance UID:', imageId);
  293. return false;
  294. }
  295. // 2. 获取 viewport 中的所有 imageIds
  296. const imageIds = viewport.getImageIds?.();
  297. if (!imageIds || imageIds.length === 0) {
  298. return false;
  299. }
  300. // 3. 遍历比较每个 imageId 的 SOP Instance UID
  301. for (const vpImageId of imageIds) {
  302. const vpSopUid = extractSopInstanceUid(vpImageId);
  303. if (vpSopUid && vpSopUid === targetSopUid) {
  304. return true;
  305. }
  306. }
  307. return false;
  308. } catch (error) {
  309. console.error('【annotationmanager】比较 SOP Instance UID 时出错:', error);
  310. return false;
  311. }
  312. }
  313. // 用 FrameOfReferenceUID 查找 viewport elements
  314. private findElementsByFOR(imageId: string): any[] {
  315. const elements: any[] = [];
  316. const renderingEngines = getRenderingEngines();
  317. if (renderingEngines) {
  318. renderingEngines.forEach((re: IRenderingEngine) => {
  319. re.getViewports().forEach((vp: IStackViewport) => {
  320. if (vp && vp.element) {
  321. const stackVp = vp as IStackViewport;
  322. if (this.hasImageBySopInstanceUid(stackVp, imageId)) {
  323. elements.push(vp.element);
  324. }
  325. }
  326. });
  327. });
  328. } else {
  329. console.warn('【annotationmanager】getRenderingEngines 方法不可用,无法查找元素');
  330. }
  331. return elements;
  332. }
  333. /**
  334. * 更新注释对象中的 URL,替换为当前服务器地址
  335. * @param annotation 注释对象
  336. * @returns 更新后的注释对象
  337. */
  338. private updateAnnotationUrls(annotation: any): any {
  339. try {
  340. const currentIpPort = getIpPort();
  341. if (!currentIpPort) {
  342. console.warn('【annotationmanager】无法获取当前服务器地址,跳过 URL 更新');
  343. return annotation;
  344. }
  345. // 正则表达式:匹配 URL 中的协议+域名/IP+端口部分
  346. // 例如:dicomweb:http://192.168.110.245:6001/path -> 提取 http://192.168.110.245:6001
  347. const urlRegex = /(https?:\/\/[^\/]+)/;
  348. // 更新 metadata.referencedImageId
  349. if (annotation.metadata?.referencedImageId) {
  350. const originalUrl = annotation.metadata.referencedImageId;
  351. const updatedUrl = originalUrl.replace(urlRegex, currentIpPort);
  352. annotation.metadata.referencedImageId = updatedUrl;
  353. console.log(`【annotationmanager】已更新 referencedImageId: ${originalUrl} -> ${updatedUrl}`);
  354. }
  355. // 更新 metadata.referencedImageURI(如果存在)
  356. if (annotation.metadata?.referencedImageURI) {
  357. const originalUrl = annotation.metadata.referencedImageURI;
  358. const updatedUrl = originalUrl.replace(urlRegex, currentIpPort);
  359. annotation.metadata.referencedImageURI = updatedUrl;
  360. console.log(`【annotationmanager】已更新 referencedImageURI: ${originalUrl} -> ${updatedUrl}`);
  361. }
  362. return annotation;
  363. } catch (error) {
  364. console.error('【annotationmanager】更新注释 URL 失败:', error);
  365. return annotation;
  366. }
  367. }
  368. private async renderAnnotations(annotationStrings: string[]): Promise<void> {
  369. console.log(`【annotationmanager】🎨 渲染 ${annotationStrings.length} 个注释`);
  370. for (const annotationString of annotationStrings) {
  371. try {
  372. // 反序列化为Cornerstone格式
  373. let deserializedAnnotation = this.serializer.deserialize(annotationString);
  374. // 更新注释中的 URL,替换为当前服务器地址
  375. deserializedAnnotation = this.updateAnnotationUrls(deserializedAnnotation);
  376. // 1. 从注释元数据中获取图像ID
  377. const imageId = deserializedAnnotation.metadata?.referencedImageId;
  378. if (!imageId) {
  379. console.warn('【annotationmanager】注释缺少 referencedImageId,跳过');
  380. continue;
  381. }
  382. // 2. 找到包含该图像的 viewport element
  383. const renderingEngines = getRenderingEngines();
  384. let targetElement: HTMLDivElement | null = null;
  385. if (renderingEngines) {
  386. for (const engine of renderingEngines) {
  387. const viewports = engine.getViewports();
  388. for (const viewport of viewports) {
  389. // 使用类型断言处理 IStackViewport
  390. const stackViewport = viewport as IStackViewport;
  391. if (stackViewport) {
  392. if (this.hasImageBySopInstanceUid(stackViewport, imageId)) {
  393. targetElement = stackViewport.element;
  394. break;
  395. }
  396. }
  397. }
  398. if (targetElement) break;
  399. }
  400. }
  401. if (!targetElement) {
  402. console.warn('【annotationmanager】未找到对应的 viewport,跳过');
  403. continue;
  404. }
  405. console.log(`【annotationmanager】渲染注释 ${JSON.stringify(deserializedAnnotation)} 到 imageid ${imageId} 的 viewport`);
  406. // 3. ✅ 使用正确的 element 参数添加注释
  407. corAnnotation.state.addAnnotation(deserializedAnnotation, targetElement);
  408. console.log(`【annotationmanager】✅ 注释渲染成功到 viewport`);
  409. } catch (error) {
  410. console.error(`【annotationmanager】❌ 注释渲染失败:`, error);
  411. // 继续处理其他注释
  412. }
  413. }
  414. }
  415. // ============ 延迟保存机制 ============
  416. /**
  417. * 安排延迟保存
  418. */
  419. private scheduleSave(annotationString: string): void {
  420. try {
  421. // 从JSON字符串中提取ID作为key
  422. const annotation = JSON.parse(annotationString);
  423. const key = annotation.annotationUID || annotation.id || `temp_${Date.now()}`;
  424. // 添加到待保存队列
  425. this.pendingChanges.set(key, annotationString);
  426. // 取消之前的延迟保存
  427. if (this.saveTimeout) {
  428. clearTimeout(this.saveTimeout);
  429. }
  430. // 设置新的延迟保存(1秒后)
  431. this.saveTimeout = setTimeout(() => {
  432. this.performBulkSave();
  433. }, 1000);
  434. } catch (error) {
  435. console.error('安排保存时解析注释失败:', error);
  436. }
  437. }
  438. /**
  439. * 执行批量保存
  440. */
  441. private async performBulkSave(): Promise<void> {
  442. if (this.pendingChanges.size === 0 || !this.activeImageId) {
  443. return;
  444. }
  445. const changes = Array.from(this.pendingChanges.values());
  446. this.pendingChanges.clear();
  447. try {
  448. console.log(`【annotationmanager】💾 批量保存 ${changes.length} 个注释`);
  449. // 调用API保存注释
  450. await annotationAPI.saveAnnotations(this.activeImageId, changes);
  451. console.log(`【annotationmanager】✅ 批量保存成功: ${changes.length} 个注释`);
  452. } catch (error) {
  453. console.error(`【annotationmanager】❌ 批量保存失败:`, error);
  454. // 保存失败时,重新加入待保存队列
  455. changes.forEach(change => {
  456. try {
  457. const annotation = JSON.parse(change);
  458. const key = annotation.annotationUID || annotation.id || `temp_${Date.now()}`;
  459. this.pendingChanges.set(key, change);
  460. } catch (error) {
  461. console.error('重新加入待保存队列时解析失败:', error);
  462. }
  463. });
  464. // 可以在这里实现重试逻辑
  465. }
  466. }
  467. }