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/primitives/carotid_imt.dart'; import 'package:fis_measure/process/primitives/detection.dart'; import 'package:fis_measure/process/primitives/location.dart'; import 'package:fis_measure/process/primitives/straightline.dart'; import 'package:fis_measure/process/visual/tissue_area.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 'package:vid/us/vid_us_unit.dart'; import 'dart:math'; import '../primitives/poyline.dart'; import 'recorder.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; List? _visuals; IMeasureItem? _activeMeasureItem; IAnnotationItem? _activeAnnotationItem; bool _canOperate = false; Size _displaySize = Size.zero; bool _isAdaptiveCarotid2D = false; Size _carotid2DSize = Size.zero; MeasureOperateType _currOpType = MeasureOperateType.measure; final Set _measureItems = {}; final Set _annotationItems = {}; late final _recorder = MeasureRecorder(this); Application(VidUsProbe probe) { _probe = probe; currentModeChanged = FEventHandler(); visualAreaChanged = FEventHandler(); canMeasureChanged = FEventHandler(); activeMeasureItemChanged = FEventHandler(); activeAnnotationItemChanged = FEventHandler(); updateRenderReady = FEventHandler(); operateTypeChanged = FEventHandler(); visualsLoaded = 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 => _frame; @override List get visuals => _visuals!; @override Set 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; } } @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); } // 通知更新事件 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 void loadFrame(VidUsImage frame) { bool frameLoaded = _frame != null; _frame = frame; if (!frameLoaded && canMeasure) { loadVisuals(); } } final FEventHandler mobileTouchEvent = 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); // 传出移动事件 } } return info; } @override void switchItem(ItemMeta meta) { switchItemByName(meta.name); } @override void switchItemByName(String name) { _updateOperateType(MeasureOperateType.measure); activeMeasureItem?.finishOnce(); // TODO: create from map if (name == MeasureTypes.Distance) { activeMeasureItem = StraightLine.createDistance( ItemMeta( MeasureTypes.Distance, measureType: MeasureTypes.Distance, description: MeasureTypes.Distance, briefAnnotation: "D", outputs: [ ItemOutputMeta(MeasureTypes.Distance, "Distance", VidUsUnit.cm), ], ), ); return; } if (name == MeasureTypes.Perimeter) { activeMeasureItem = PolyLine.createPerimeter( ItemMeta( MeasureTypes.Perimeter, measureType: MeasureTypes.Perimeter, description: MeasureTypes.Perimeter, briefAnnotation: MeasureTypes.Perimeter, outputs: [ ItemOutputMeta(MeasureTypes.Perimeter, "Perimeter", VidUsUnit.cm), ], ), null, ); return; } if (name == MeasureTypes.Area) { activeMeasureItem = PolyLine.createArea( ItemMeta( MeasureTypes.Area, measureType: MeasureTypes.Area, description: MeasureTypes.Area, briefAnnotation: MeasureTypes.Area, outputs: [ ItemOutputMeta(MeasureTypes.Area, "Area", VidUsUnit.cm2), ], ), null, ); return; } if (name == MeasureTypes.Depth) { final isProbeConvex = (currentVisualArea as TissueArea).isConvex; final Location Function(ItemMeta, [IMeasureItem?]) fn = isProbeConvex ? Location.createTissueConvexDepth : Location.createTissueDepth; activeMeasureItem = fn( ItemMeta( MeasureTypes.Depth, measureType: MeasureTypes.Depth, description: MeasureTypes.Depth, briefAnnotation: MeasureTypes.Depth, outputs: [ ItemOutputMeta(MeasureTypes.Depth, "Depth", VidUsUnit.cm), ], ), null); return; } if (name == MeasureTypes.AntCCA_IMT) { activeMeasureItem = CarotidIMT.createMeasureRect( ItemMeta( MeasureTypes.AntCCA_IMT, description: MeasureTypes.AntCCA_IMT, measureType: MeasureTypes.AntCCA_IMT, outputs: [ ItemOutputMeta(MeasureTypes.AntCCA_IMT, "", VidUsUnit.cm), ], ), null); return; } if (name == MeasureTypes.PostCCA_IMT) { activeMeasureItem = CarotidIMT.createMeasureRect( ItemMeta( MeasureTypes.PostCCA_IMT, description: MeasureTypes.PostCCA_IMT, measureType: MeasureTypes.PostCCA_IMT, outputs: [ ItemOutputMeta(MeasureTypes.PostCCA_IMT, "", VidUsUnit.cm), ], ), null); return; } if (name == MeasureTypes.BothCCA_IMT) { activeMeasureItem = CarotidIMT.createMeasureRect( ItemMeta( MeasureTypes.BothCCA_IMT, description: MeasureTypes.BothCCA_IMT, measureType: MeasureTypes.BothCCA_IMT, outputs: [ ItemOutputMeta(MeasureTypes.BothCCA_IMT, "", VidUsUnit.cm), ], ), null); return; } if (name == MeasureTypes.PlaqueDetection) { ///TODO: 只允许触发一次 activeMeasureItem = CarotidDetection.createDetectionRect( ItemMeta( MeasureTypes.PlaqueDetection, description: MeasureTypes.PlaqueDetection, measureType: MeasureTypes.PlaqueDetection, outputs: [ ItemOutputMeta(MeasureTypes.PlaqueDetection, "", VidUsUnit.cm2), ], ), null); return; } if (name == MeasureTypes.IntimaDetection) { ///TODO: 只允许触发一次 activeMeasureItem = CarotidDetection.createDetectionRect( ItemMeta( MeasureTypes.IntimaDetection, description: MeasureTypes.IntimaDetection, measureType: MeasureTypes.IntimaDetection, outputs: [ ItemOutputMeta(MeasureTypes.IntimaDetection, "", VidUsUnit.cm2), ], ), null); return; } if (name == MeasureTypes.Volume) { final childBuild = (String name) { return ItemMeta( name, description: name, outputs: [ ItemOutputMeta("Distance", "Distance", VidUsUnit.cm), ], ); }; activeMeasureItem = MeasureItemFactory.createItem( ItemMeta( MeasureTypes.Volume, measureType: MeasureTypes.Volume, description: MeasureTypes.Volume, outputs: [ ItemOutputMeta("Volume", "Volume", VidUsUnit.cm2), ], multiMethod: "LWH", childItems: [ childBuild("L"), childBuild("W"), childBuild("H"), ], ), ); return; } activeMeasureItem = null; } @override void switchAnnotation([AnnotationType? type, String? text]) { _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) { _handleAreaSwitch(area, PointInfo(0, 0, PointInfoType.mouseDown)); } } } @override void switchVisual(int indicator) { if (_visuals == null) return; for (var i = 0; i < _visuals!.length; i++) { final v = _visuals![i]; if (i == indicator) { v.visualAreas.first.isActive = true; } else { v.setUnAcitve(); } } } @override void undoRecord() { if (_recorder.undoOnce()) { _updateRender(); } } @override void clearRecords() { _recorder.clear(); _updateRender(); } @protected List convertVisuals() { return VisualsLoader(frameData!.visuals).load(); } 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); _clear(); if (canMeasure) { if (frameData != null) { loadVisuals(); } } } void _onActiveMeasureItemFeatureUpdated( Object sender, IMeasureItemFeature? e, ) { _updateRender(); } void _onActiveAnnotationItemFeatureUpdated( Object sender, IAnnotationItemFeature? e, ) { _updateRender(); } @protected void loadVisuals() { _clearVisuals(); _visuals = convertVisuals(); // 默认第一个区域为活动域 switchVisual(0); visualsLoaded.emit(this, null); } void _clear() { for (var item in measureItems) { item.clear(); } _clearVisuals(); } void _clearVisuals() { _visuals = []; } IVisualArea? _attchVisualArea(PointInfo point) { 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; } /// 点击切换所在区域焦点 if (area != currentVisualArea) { for (var visual in visuals) { visual.setUnAcitve(); } area.isActive = true; switchItemByName("---"); //TODO: visualAreaChanged.emit(this, area); return true; } return false; } }