import 'package:fis_common/logger/logger.dart'; import 'package:fis_measure/interfaces/date_types/rect_region.dart'; import 'package:fis_measure/interfaces/enums/annotation.dart'; import 'package:fis_measure/interfaces/enums/operate.dart'; import 'package:fis_measure/interfaces/process/annotations/annotation.dart'; import 'package:fis_measure/interfaces/process/items/item.dart'; import 'package:fis_measure/interfaces/process/items/item_feature.dart'; import 'package:fis_measure/interfaces/process/items/item_metas.dart'; import 'package:fis_measure/interfaces/process/items/types.dart'; import 'package:fis_measure/interfaces/process/visuals/visual_area.dart'; import 'package:fis_measure/interfaces/process/visuals/visual.dart'; import 'package:fis_measure/interfaces/process/viewports/viewport.dart'; import 'package:fis_measure/interfaces/process/modes/mode.dart'; import 'package:fis_common/event/event_type.dart'; import 'package:fis_measure/interfaces/process/workspace/application.dart'; import 'package:fis_measure/interfaces/process/workspace/point_info.dart'; import 'package:fis_measure/interfaces/process/workspace/recorder.dart'; import 'package:fis_measure/process/annotations/arrow_annotation.dart'; import 'package:fis_measure/process/annotations/input_annotation.dart'; import 'package:fis_measure/process/annotations/label_annotation.dart'; import 'package:fis_measure/process/items/factory.dart'; import 'package:fis_measure/process/items/top_item.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; import 'package:vid/us/vid_us_image.dart'; import 'package:vid/us/vid_us_probe.dart'; import 'dart:math'; import 'recorder.dart'; import 'third_part/application.dart'; import 'visual_loader.dart'; class Application implements IApplication { // ignore: constant_identifier_names static const C_VID_THIRDPART_NAME = "ThirdPart"; late VidUsProbe _probe; VidUsImage? _frame; VidUsImage? _subFrame; List? _visuals; IMeasureItem? _activeMeasureItem; IAnnotationItem? _activeAnnotationItem; bool _canOperate = false; Size _displaySize = Size.zero; bool _isAdaptiveCarotid2D = false; bool _isSingleFrame = true; Size _carotid2DSize = Size.zero; MeasureOperateType _currOpType = MeasureOperateType.measure; final List _measureItems = []; final Set _annotationItems = {}; late final _recorder = MeasureRecorder(this); final emptyItem = ItemMeta( "EmptyItem", measureType: MeasureTypes.Empty, description: "", outputs: [], ); Application(VidUsProbe probe) { _probe = probe; currentModeChanged = FEventHandler(); visualAreaChanged = FEventHandler(); canMeasureChanged = FEventHandler(); activeMeasureItemChanged = FEventHandler(); activeAnnotationItemChanged = FEventHandler(); updateRenderReady = FEventHandler(); operateTypeChanged = FEventHandler(); visualsLoaded = FEventHandler(); displaySizeChanged = FEventHandler(); } @override bool get canMeasure => _canOperate; @override set canMeasure(bool value) { if (value != _canOperate) { _canOperate = value; _doCanMeasureChanged(); } } /// 是否扇形探头 bool get isProbeConvex => probe.type == VidUsProbeType.Convex; @override VidUsProbe get probe => _probe; @override String get applicationName => _probe.application.applicationName; @override String get categoryName => _probe.application.applicationCategoryName; @override bool get isThirdPart => probe.name == C_VID_THIRDPART_NAME; @override IMode get currentMode => currentVisualArea.mode; @override IViewPort get currentViewPort => currentVisualArea.viewport!; @override IVisual get currentVisual => visuals.firstWhere((e) => e.activeArea != null); @override IVisualArea get currentVisualArea => currentVisual.activeArea!; @override VidUsImage? get frameData => _subFrame ?? _frame; @override List get visuals => _visuals ?? []; @override List get measureItems => _measureItems; @override Set get annotationItems => _annotationItems; @override MeasureOperateType get currentOperateType => _currOpType; @override Size get displaySize => _displaySize; @override set displaySize(Size value) { if (value != _displaySize) { _displaySize = value; displaySizeChanged.emit(this, value); } } @override bool get isSingleFrame => _isSingleFrame; @override set isSingleFrame(bool value) { if (value != _isSingleFrame) { _isSingleFrame = value; } } @override double get displayScaleRatio { if (isAdaptiveCarotid2D) { final firstScale = min(displaySize.width / frameData!.width, displaySize.height / frameData!.height); final secondScale = min(frameData!.width / carotid2DSize.width, frameData!.height / carotid2DSize.height); return firstScale * secondScale; } if (frameData != null) { return min(displaySize.width / frameData!.width, displaySize.height / frameData!.height); } return 1.0; } @override List get avaliableModes { final modes = []; for (var visual in visuals) { modes.addAll(visual.modes); } return modes; } @override bool get isAdaptiveCarotid2D => _isAdaptiveCarotid2D; @override set isAdaptiveCarotid2D(bool value) { if (value != _isAdaptiveCarotid2D) { _isAdaptiveCarotid2D = value; //[Carotid] ✅如果颈动脉2D图像超出范围需要缩放,利用 layoutRegion 参数改变缩放比 if (value) { final scale = min(frameData!.width / carotid2DSize.width, frameData!.height / carotid2DSize.height); currentVisualArea.layoutRegion = RectRegion.fill(0, 0, scale, scale); } else { currentVisualArea.layoutRegion = RectRegion.fill(0, 0, 1, 1); } } } @override Size get carotid2DSize => _carotid2DSize; @override set carotid2DSize(Size value) { if (value != _carotid2DSize) { _carotid2DSize = value; } } @override IMeasureItem? get activeMeasureItem => _activeMeasureItem; set activeMeasureItem(IMeasureItem? value) { if (value != _activeMeasureItem) { // 解绑失活测量项事件监听 _activeMeasureItem?.featureUpdated .removeListener(_onActiveMeasureItemFeatureUpdated); _activeMeasureItem = value; if (_activeMeasureItem != null) { _measureItems.add(_activeMeasureItem!); // 添加活动测量项事件监听 _activeMeasureItem!.featureUpdated .addListener(_onActiveMeasureItemFeatureUpdated); } if (_subFrame != null) { // 清除附帧并恢复原始帧的Visual状态 _subFrame = null; loadVisuals(); } // 通知更新事件 activeMeasureItemChanged.emit(this, value); _updateRender(); } } @override IAnnotationItem? get activeAnnotationItem => _activeAnnotationItem; set activeAnnotationItem(IAnnotationItem? value) { if (value != _activeAnnotationItem) { // 解绑失活注释项事件监听 _activeAnnotationItem?.featureUpdated .removeListener(_onActiveAnnotationItemFeatureUpdated); _activeAnnotationItem = value; if (_activeAnnotationItem != null) { _annotationItems.add(_activeAnnotationItem!); // 添加活动注释项事件监听 _activeAnnotationItem!.featureUpdated .addListener(_onActiveAnnotationItemFeatureUpdated); } // 通知更新事件 activeAnnotationItemChanged.emit(this, value); _updateRender(); } } /// 是否注释模式工作中 bool get isAnnotationWorking => currentOperateType == MeasureOperateType.annotation; @override IMeasureRecorder get recorder => _recorder; @override late final FEventHandler currentModeChanged; @override late final FEventHandler visualAreaChanged; @override late final FEventHandler canMeasureChanged; @override late final FEventHandler activeMeasureItemChanged; @override late final FEventHandler activeAnnotationItemChanged; @override late final FEventHandler updateRenderReady; @override late final FEventHandler operateTypeChanged; @override late final FEventHandler visualsLoaded; @override late final FEventHandler displaySizeChanged; @override void loadFrame(VidUsImage frame, [bool clearable = true]) { if (_frame == null) { _frame = frame; if (clearable) { _resetFrameCache(); } return; } bool isSameIndex = _frame?.index == frame.index; if (isSameIndex) { _frame = frame; return; } if (activeMeasureItem != null && activeMeasureItem! is ITopMeasureItem) { if ((activeMeasureItem as ITopMeasureItem).isCrossFrameMode) { // _frame = VidUsImage( // _frame!.index, // frame.width, // frame.height, // frame.imageData, // ); // TODO: 暂不支持直接改index _subFrame = frame; // 仅更新frame数据 loadVisuals(); return; } } _frame = frame; if (clearable) { _resetFrameCache(); } //如果clearable为false,需要手动调用loadCarotidVisuals加载visuals } void _resetFrameCache() { _clearFrameCache(); if (canMeasure) { loadVisuals(); } } final FEventHandler mobileTouchEvent = FEventHandler(); final FEventHandler mobileTouchEndEvent = FEventHandler(); @override PointInfo createPointInfo(Offset offset, PointInfoType type) { if (frameData == null) { throw NullThrownError(); } final width = displaySize.width; final height = displaySize.height; final x = offset.dx / width; final y = offset.dy / height; final percentOffset = Offset(x, y); final info = PointInfo.fromOffset(percentOffset, type); final matchArea = _attchVisualArea(info); if (matchArea != null) { if (matchArea != currentVisualArea) { bool focusAreaChanged = _handleAreaSwitch(matchArea, info); // if (focusAreaChanged) { // // 焦点区域发生变更,不继续执行操作 // return info; // } return info; } } info.hostVisualArea ??= currentVisualArea; // 未切换区域则沿用当前区域 if (isAnnotationWorking) { activeAnnotationItem?.execute(info); } else { activeMeasureItem?.execute(info); if (type == PointInfoType.touchMove) { mobileTouchEvent.emit(this, offset); // 传出移动事件 } if (type == PointInfoType.touchUp) { mobileTouchEndEvent.emit(this, offset); // 传出触摸结束事件 } } return info; } @override bool switchItem(ItemMeta meta) { _updateOperateType(MeasureOperateType.measure); _handleBeforeSwitchItem(); activeMeasureItem = MeasureItemFactory.createItem(meta); return activeMeasureItem != null; } @override void autoStartAgain(ItemMeta meta) { if (activeMeasureItem == null) return; final item = activeMeasureItem!; if (item.feature != null) { item.finishOnce(); } activeMeasureItem = MeasureItemFactory.createItem(meta); } @override void switchAnnotation([AnnotationType? type, String? text]) { _handleBeforeExitMeasure(); switchItem(emptyItem); _updateOperateType(MeasureOperateType.annotation); final targetType = type ?? AnnotationType.input; if (activeAnnotationItem != null && activeAnnotationItem!.type == targetType && activeAnnotationItem!.text == text) { return; } // activeAnnotationItem?.finishLast(); final cachedItems = annotationItems.toList(); final cachedItemIdx = cachedItems.indexWhere((e) => e.type == targetType); if (cachedItemIdx > -1) { activeAnnotationItem = cachedItems[cachedItemIdx]; } else { switch (targetType) { case AnnotationType.label: activeAnnotationItem = LabelAnnotation(); break; case AnnotationType.input: activeAnnotationItem = InputAnnotation(); break; case AnnotationType.arrow: activeAnnotationItem = ArrowAnnotation(); break; } cachedItems.add(activeAnnotationItem!); } activeAnnotationItem?.text = text; activeAnnotationItemChanged.emit(this, activeAnnotationItem); } @override void switchMode(String name) { for (var area in currentVisual.visualAreas) { if (area.mode.name == name) { _changeAcitveArea(area); } } } @override void switchVisual(int indicator) { if (_visuals == null) return; try { for (var i = 0; i < _visuals!.length; i++) { final v = _visuals![i]; if (i == indicator) { v.visualAreas.first.isActive = true; } else { v.setUnAcitve(); } } } catch (e) { logger.e('switch error: $e'); } } @override void undoRecord() { if (_recorder.undoOnce()) { _updateRender(); } } @override void clearRecords() { _recorder.clear(); _updateRender(); } /// 判断是否第三方图像且参考信息为空 @override bool checkIs3rdAndEmptyStandardLine() { if (isThirdPart == false) return false; final standardLine = (this as ThirdPartApplication).standardLine; return standardLine.currentPixelSpacing.isEmpty; } @protected List convertVisuals() { return VisualsLoader(frameData!.visuals).load(); } @protected void loadVisuals() { _clearVisuals(); _visuals = convertVisuals(); // 默认第一个区域为活动域 switchVisual(0); visualsLoaded.emit(this, null); } @override void loadCarotidVisuals(VidUsImage carotidVid) { _clearVisuals(); _visuals = VisualsLoader(carotidVid.visuals).load(); switchVisual(0); visualsLoaded.emit(this, null); } void _handleBeforeSwitchItem() { if (activeMeasureItem == null) return; final item = activeMeasureItem!; if (item is TopMeasureItem && item.feature != null) { bool isAllEmpty = item.childItems.every((e) => e.measuredFeatures.isEmpty); if (isAllEmpty) { _recorder.undoOnce(); } } if (item.feature != null) { if (item.finishAfterUnactive) { item.finishOnce(); } else { _recorder.undoOnce(); } } } // 离开测量模式前处理 void _handleBeforeExitMeasure() { if (activeMeasureItem == null) return; final item = activeMeasureItem!; if (item is TopMeasureItem && item.feature != null) { bool isAllEmpty = item.childItems.every((e) => e.measuredFeatures.isEmpty); if (isAllEmpty) { _recorder.undoOnce(); } } if (item.feature != null) { _recorder.undoOnce(); } } void _clearFrameCache() { _recorder.clear(); _annotationItems.clear(); _clearVisuals(); } void _updateOperateType(MeasureOperateType type) { if (currentOperateType == MeasureOperateType.annotation) { activeAnnotationItem?.finishLast(); } if (currentOperateType != type) { _currOpType = type; operateTypeChanged.emit(this, type); } } void _updateRender() { updateRenderReady.emit(this, null); } void _doCanMeasureChanged() { canMeasureChanged.emit(this, canMeasure); _clearFrameCache(); if (canMeasure) { if (frameData != null) { loadVisuals(); } } } void _onActiveMeasureItemFeatureUpdated( Object sender, IMeasureItemFeature? e, ) { _updateRender(); } void _onActiveAnnotationItemFeatureUpdated( Object sender, IAnnotationItemFeature? e, ) { _updateRender(); } void _clearVisuals() { _visuals = []; } IVisualArea? _attchVisualArea(PointInfo point) { if (currentVisualArea.displayRegion.containsPoint(point)) { return currentVisualArea; } for (var visual in visuals) { for (var area in visual.visualAreas) { if (area.displayRegion.containsPoint(point)) { return area; } } } return null; } bool _handleAreaSwitch(IVisualArea area, PointInfo point) { if (point.pointType != PointInfoType.mouseDown && point.pointType != PointInfoType.touchDown) { return false; } // 点击切换所在区域焦点 // 不同幅或者不同模式类型的才切换 bool needSwitch = false; if (area.visual.visualData.indicator != currentVisual.visualData.indicator) { needSwitch = true; } else { if (!currentVisualArea.displayRegion.containsPoint(point)) { needSwitch = true; } } if (needSwitch) { _changeAcitveArea(area); return true; } return false; } void _changeAcitveArea(IVisualArea area) { for (var visual in visuals) { visual.setUnAcitve(); } area.isActive = true; visualAreaChanged.emit(this, area); } }