Browse Source

半自动/自动测量项提交

guanxinyi 7 months ago
parent
commit
f74fb8d533

+ 4 - 0
lib/interfaces/process/items/item_metas.dart

@@ -20,6 +20,9 @@ class ItemMeta {
   /// 子项元信息集合
   List<ItemMeta> childItems;
 
+  /// 多测量方法
+  List<ItemMeta> multiMethodItems;
+
   /// 当前测量项的购买状态
   /// 0: Free 免费
   /// 1: Probation 试用中
@@ -39,6 +42,7 @@ class ItemMeta {
     this.rimWidth,
     this.briefAnnotation = '',
     this.childItems = const [],
+    this.multiMethodItems = const [],
   });
 
   /// 根据名称获取子项

+ 3 - 0
lib/interfaces/process/items/types.dart

@@ -147,6 +147,9 @@ class MeasureTypes {
 
   /// 半自动跟踪
   static const SemiautoTrace = "SemiautoTrace";
+
+  /// 自动跟踪
+  static const AutoDopplerTrace = "AutoDopplerTrace";
   static const DopplerTrace = "DopplerTrace";
   static const SlopeDoppler = "SlopeDoppler";
   static const SV = "Sv";

+ 666 - 0
lib/process/calcuators/auto_doppler_trace.dart

@@ -0,0 +1,666 @@
+// ignore_for_file: invalid_use_of_protected_member
+
+import 'package:fis_measure/interfaces/date_types/point.dart';
+import 'package:fis_measure/interfaces/process/items/terms.dart';
+import 'package:fis_measure/process/calcuators/formulas/cardiac.dart';
+import 'package:fis_measure/process/calcuators/formulas/general.dart';
+import 'package:fis_measure/process/items/item_feature.dart';
+import 'package:fis_measure/process/primitives/multi_method/auto_doppler_trace.dart';
+import 'package:fis_measure/process/primitives/multi_method/dop_trace_disp/cardiac_cycle.dart';
+import 'package:fis_measure/process/primitives/multi_method/dop_trace_disp/data.dart';
+import 'dart:math' as math;
+import 'calculator.dart';
+
+class AutoDopplerTraceCal extends Calculator<TraceItemAbstract, double> {
+  AutoDopplerTraceCal(TraceItemAbstract ref) : super(ref);
+  double cyclesStart = double.maxFinite;
+  double cyclesEnd = 0;
+  int validSystoleCyclesLength = 0;
+
+  @override
+  void calculate() {
+    if (ref.feature == null) return;
+
+    Map<String, double> calculateDopplerTraceResult = {};
+
+    AutoDopplerTraceFeature feature = ref.feature! as AutoDopplerTraceFeature;
+
+    /// 最大点集
+    List<DPoint> maxPhysicalPonints = TraceListData.aboveMaxPonints;
+
+    List<CardiacCycle> validSystoleCycles = feature.currentCardiacCycleList;
+
+    if (validSystoleCycles.isEmpty) {
+      List<DPoint> regionPoints = feature.innerPoints
+          .map((e) => convertTimeMotionPoint(feature, e))
+          .toList();
+      final yFlippedPoints = regionPoints;
+      double min = math.min(yFlippedPoints.first.x, yFlippedPoints.last.x);
+      double max = math.max(yFlippedPoints.first.x, yFlippedPoints.last.x);
+      calculateDopplerTraceResult = calculateDopplerTrace(
+        yFlippedPoints,
+        min,
+        max,
+      );
+    }
+
+    if (validSystoleCyclesLength != validSystoleCycles.length) {
+      print(validSystoleCyclesLength);
+      for (int i = 0; i < validSystoleCycles.length; i++) {
+        /// 获取物理点坐标
+        List<DPoint> regionPoints = getPoints(
+          maxPhysicalPonints,
+          validSystoleCycles[i],
+        );
+        final points = regionPoints.map((e) {
+          final currentPoint = convertTimeMotionPoint(feature, e);
+          return DPoint(currentPoint.x, currentPoint.y);
+        }).toList();
+        if (points.isEmpty) {
+          return;
+        }
+        double min = math.min(points.first.x, points.last.x);
+        double max = math.max(points.first.x, points.last.x);
+
+        calculateDopplerTraceResult = calculateDopplerTrace(
+          points,
+          min,
+          max,
+          validSystoleCycles[i],
+        );
+      }
+
+      if (validSystoleCycles.isEmpty) {
+        feature.initValues();
+      }
+      validSystoleCyclesLength = validSystoleCycles.length;
+    }
+
+    for (var output in ref.meta.outputs) {
+      ///TODO:[Gavin] 实现以下计算逻辑
+      switch (output.name) {
+        case MeasureTerms.Placeholder:
+          feature.updateStringValue(output, "", output.unit);
+          break;
+        case MeasureTerms.TAMAX:
+          if (calculateDopplerTraceResult[MeasureTerms.TAMAX] != null) {
+            feature.updateFloatValue(output,
+                calculateDopplerTraceResult[MeasureTerms.TAMAX]!, output.unit);
+          }
+          break;
+        case MeasureTerms.TAMEAN:
+          if (calculateDopplerTraceResult[MeasureTerms.TAMEAN] != null) {
+            feature.updateFloatValue(output,
+                calculateDopplerTraceResult[MeasureTerms.TAMEAN]!, output.unit);
+          }
+          break;
+        case MeasureTerms.PS:
+          if (calculateDopplerTraceResult[MeasureTerms.PS] != null) {
+            feature.updateFloatValue(output,
+                calculateDopplerTraceResult[MeasureTerms.PS]!, output.unit);
+          }
+          break;
+        case MeasureTerms.ED:
+          if (calculateDopplerTraceResult[MeasureTerms.ED] != null) {
+            feature.updateFloatValue(output,
+                calculateDopplerTraceResult[MeasureTerms.ED]!, output.unit);
+          }
+          break;
+        case MeasureTerms.MD:
+          if (calculateDopplerTraceResult[MeasureTerms.MD] != null) {
+            feature.updateFloatValue(output,
+                calculateDopplerTraceResult[MeasureTerms.MD]!, output.unit);
+          }
+          break;
+        case MeasureTerms.HeartRate:
+          if (calculateDopplerTraceResult[MeasureTerms.HeartRate] != null) {
+            feature.updateFloatValue(
+                output,
+                calculateDopplerTraceResult[MeasureTerms.HeartRate]!,
+                output.unit);
+          }
+          break;
+        case MeasureTerms.Acceleration:
+          if (calculateDopplerTraceResult[MeasureTerms.Acceleration] != null) {
+            feature.updateFloatValue(
+                output,
+                calculateDopplerTraceResult[MeasureTerms.Acceleration]!,
+                output.unit);
+          }
+          break;
+        case MeasureTerms.AT:
+          if (calculateDopplerTraceResult[MeasureTerms.AT] != null) {
+            feature.updateFloatValue(output,
+                calculateDopplerTraceResult[MeasureTerms.AT]!, output.unit);
+          }
+          break;
+        case MeasureTerms.PSED:
+          if (calculateDopplerTraceResult[MeasureTerms.PSED] != null) {
+            feature.updateFloatValue(output,
+                calculateDopplerTraceResult[MeasureTerms.PSED]!, output.unit);
+          }
+          break;
+        case MeasureTerms.EDPS:
+          if (calculateDopplerTraceResult[MeasureTerms.EDPS] != null) {
+            feature.updateFloatValue(output,
+                calculateDopplerTraceResult[MeasureTerms.EDPS]!, output.unit);
+          }
+          break;
+        case MeasureTerms.PI:
+          if (calculateDopplerTraceResult[MeasureTerms.PI] != null) {
+            feature.updateFloatValue(output,
+                calculateDopplerTraceResult[MeasureTerms.PI]!, output.unit);
+          }
+          break;
+        case MeasureTerms.PIMD:
+          if (calculateDopplerTraceResult[MeasureTerms.PIMD] != null) {
+            feature.updateFloatValue(output,
+                calculateDopplerTraceResult[MeasureTerms.PIMD]!, output.unit);
+          }
+          break;
+        case MeasureTerms.RI:
+          if (calculateDopplerTraceResult[MeasureTerms.RI] != null) {
+            feature.updateFloatValue(output,
+                calculateDopplerTraceResult[MeasureTerms.RI]!, output.unit);
+          }
+          break;
+        case MeasureTerms.RIMD:
+          if (calculateDopplerTraceResult[MeasureTerms.RIMD] != null) {
+            feature.updateFloatValue(output,
+                calculateDopplerTraceResult[MeasureTerms.RIMD]!, output.unit);
+          }
+          break;
+        case MeasureTerms.MaxPG:
+          if (calculateDopplerTraceResult[MeasureTerms.MaxPG] != null) {
+            feature.updateFloatValue(output,
+                calculateDopplerTraceResult[MeasureTerms.MaxPG]!, output.unit);
+          }
+          break;
+        case MeasureTerms.VelocityMax:
+          if (calculateDopplerTraceResult[MeasureTerms.VelocityMax] != null) {
+            feature.updateFloatValue(
+                output,
+                calculateDopplerTraceResult[MeasureTerms.VelocityMax]!,
+                output.unit);
+          }
+          break;
+        case MeasureTerms.VelocityMean:
+          if (calculateDopplerTraceResult[MeasureTerms.VelocityMean] != null) {
+            feature.updateFloatValue(
+                output,
+                calculateDopplerTraceResult[MeasureTerms.VelocityMean]!,
+                output.unit);
+          }
+          break;
+        case MeasureTerms.PeakPG:
+          if (calculateDopplerTraceResult[MeasureTerms.PeakPG] != null) {
+            feature.updateFloatValue(output,
+                calculateDopplerTraceResult[MeasureTerms.PeakPG]!, output.unit);
+          }
+          break;
+        case MeasureTerms.VTI:
+          if (calculateDopplerTraceResult[MeasureTerms.VTI] != null) {
+            feature.updateFloatValue(output,
+                calculateDopplerTraceResult[MeasureTerms.VTI]!, output.unit);
+          }
+          break;
+        case MeasureTerms.VTIMean:
+          if (calculateDopplerTraceResult[MeasureTerms.VTIMean] != null) {
+            feature.updateFloatValue(
+                output,
+                calculateDopplerTraceResult[MeasureTerms.VTIMean]!,
+                output.unit);
+          }
+          break;
+        case MeasureTerms.MPG:
+          if (calculateDopplerTraceResult[MeasureTerms.MPG] != null) {
+            feature.updateFloatValue(output,
+                calculateDopplerTraceResult[MeasureTerms.MPG]!, output.unit);
+          }
+          break;
+        case MeasureTerms.MMPG:
+          if (calculateDopplerTraceResult[MeasureTerms.MMPG] != null) {
+            feature.updateFloatValue(output,
+                calculateDopplerTraceResult[MeasureTerms.MMPG]!, output.unit);
+          }
+          break;
+        case MeasureTerms.TiEnv:
+          // var outputTiEnv = GeneralFormulas.countEnvelopeTime(points);
+          if (calculateDopplerTraceResult[MeasureTerms.TiEnv] != null) {
+            feature.updateFloatValue(output,
+                calculateDopplerTraceResult[MeasureTerms.TiEnv]!, output.unit);
+          }
+          break;
+        case MeasureTerms.EVEL:
+          if (calculateDopplerTraceResult[MeasureTerms.EVEL] != null) {
+            feature.updateFloatValue(output,
+                calculateDopplerTraceResult[MeasureTerms.EVEL]!, output.unit);
+          }
+          break;
+        case MeasureTerms.AVEL:
+          if (calculateDopplerTraceResult[MeasureTerms.AVEL] != null) {
+            feature.updateFloatValue(output,
+                calculateDopplerTraceResult[MeasureTerms.AVEL]!, output.unit);
+          }
+          break;
+        case MeasureTerms.EARatio:
+          if (calculateDopplerTraceResult[MeasureTerms.EARatio] != null) {
+            feature.updateFloatValue(
+                output,
+                calculateDopplerTraceResult[MeasureTerms.EARatio]!,
+                output.unit);
+          }
+          break;
+        case MeasureTerms.DT:
+          if (calculateDopplerTraceResult[MeasureTerms.DT] != null) {
+            feature.updateFloatValue(output,
+                calculateDopplerTraceResult[MeasureTerms.DT]!, output.unit);
+          }
+          break;
+        case MeasureTerms.PHT:
+          if (calculateDopplerTraceResult[MeasureTerms.PHT] != null) {
+            feature.updateFloatValue(output,
+                calculateDopplerTraceResult[MeasureTerms.PHT]!, output.unit);
+          }
+          break;
+        case MeasureTerms.VA:
+          if (calculateDopplerTraceResult[MeasureTerms.VA] != null) {
+            feature.updateFloatValue(output,
+                calculateDopplerTraceResult[MeasureTerms.VA]!, output.unit);
+          }
+          break;
+        case MeasureTerms.ADur:
+          if (calculateDopplerTraceResult[MeasureTerms.ADur] != null) {
+            feature.updateFloatValue(output,
+                calculateDopplerTraceResult[MeasureTerms.ADur]!, output.unit);
+          }
+          break;
+        case MeasureTerms.ATDTRatio:
+          if (calculateDopplerTraceResult[MeasureTerms.ATDTRatio] != null) {
+            feature.updateFloatValue(
+                output,
+                calculateDopplerTraceResult[MeasureTerms.ATDTRatio]!,
+                output.unit);
+          }
+          break;
+        case MeasureTerms.ATETRatio:
+          if (calculateDopplerTraceResult[MeasureTerms.ATETRatio] != null) {
+            feature.updateFloatValue(
+                output,
+                calculateDopplerTraceResult[MeasureTerms.ATETRatio]!,
+                output.unit);
+          }
+          break;
+        // case MeasureTerms.Trace:
+        //   if (calculateDopplerTraceResult[MeasureTerms.TAMAX] != null) {
+
+        //   break;
+        default:
+          break;
+      }
+    }
+  }
+
+  List<DPoint> getPoints(
+    List<DPoint> lines,
+    CardiacCycle cycle,
+  ) {
+    // Size displaySize = Get.find<IApplication>().displaySize;
+    List<DPoint> points = [];
+    for (DPoint line in lines) {
+      // DPoint logicPoint = convert2LogicPoint(line);
+      final systoleStartX = cycle.systoleStart.x;
+      final diastoleEndX = cycle.diastoleEnd.x;
+      if (line.x >= systoleStartX && line.x <= diastoleEndX) {
+        points.add(DPoint(line.x, line.y));
+      }
+    }
+    return points;
+  }
+
+  List<DPoint> getFeatureYFlippedPoints(MeasureItemFeature feature) {
+    final regionPoints = feature.innerPoints
+        .map((e) => convertTimeMotionPoint(feature, e))
+        .toList();
+    return regionPoints;
+  }
+
+  List<double> getCountVTI(MeasureItemFeature feature) {
+    final yFlippedPoints = getFeatureYFlippedPoints(feature);
+    final result = GeneralFormulas.countVTI(yFlippedPoints);
+    return result;
+  }
+
+  /// 获取到值之后
+  Map<String, double> calculateHeartCycleRelevantValues(
+    CardiacCycle heartCycle,
+    List<DPoint> maxTraceLineOfCycle,
+    List<DPoint> meanTraceLineOfCycle,
+  ) {
+    double pv = double.nan;
+    double vtiMean = double.nan;
+    double taMean = double.nan;
+    double mmpg = double.nan;
+    if (meanTraceLineOfCycle.isNotEmpty) {
+      List<double> meanResult = GeneralFormulas.countVTI(meanTraceLineOfCycle);
+      vtiMean = meanResult.first;
+      taMean = meanResult[2];
+    }
+
+    double vti = double.nan;
+    double taMax = double.nan;
+    double mpg = double.nan;
+    double ps = double.nan;
+    double ed = double.nan;
+    double md = double.nan;
+    double acctime = double.nan;
+    double accel = double.nan;
+    if (maxTraceLineOfCycle.isNotEmpty) {
+      List<double> vtiResult = GeneralFormulas.countVTI(maxTraceLineOfCycle);
+
+      vti = vtiResult.first;
+      ps = heartCycle.peakSystolic.y;
+      ed = heartCycle.diastoleEnd.y;
+      md = heartCycle.minimumAbsoluteVelocity.y;
+      taMax = vtiResult[2];
+      pv = vtiResult[3];
+      mpg = vtiResult[4];
+      mmpg = vtiResult[4];
+      acctime = heartCycle.peakSystolic.x - heartCycle.systoleStart.x;
+      accel = (heartCycle.peakSystolic.y - heartCycle.systoleStart.y).abs() /
+          acctime;
+    }
+    double velE = double.nan;
+    double velA = double.nan;
+    double dt = double.nan;
+    double pht = double.nan;
+    double va = double.nan;
+    double aDur = double.nan;
+    double eAration = double.nan;
+
+    if (heartCycle.ePeak != DPoint.zero && heartCycle.ePeak != null) {
+      velE = heartCycle.ePeak!.y;
+      if (heartCycle.ePeakEnd != DPoint.zero &&
+          heartCycle.ePeak != DPoint.zero) {
+        acctime = heartCycle.ePeak!.x - heartCycle.systoleStart.x;
+        accel =
+            (heartCycle.ePeak!.y - heartCycle.systoleStart.y).abs() / acctime;
+      }
+    }
+    if (heartCycle.aPeak != DPoint.zero && heartCycle.aPeak != null) {
+      velA = heartCycle.aPeak!.y;
+    }
+    if (heartCycle.ePeak != DPoint.zero && heartCycle.aPeak != DPoint.zero) {
+      eAration = (velE - velA).abs();
+    }
+    if (heartCycle.ePeak != DPoint.zero &&
+        heartCycle.ePeakEnd != DPoint.zero &&
+        heartCycle.aPeak != null &&
+        heartCycle.ePeakEnd != null) {
+      dt = (heartCycle.ePeakEnd!.x - heartCycle.ePeak!.x).abs();
+      pht = CardiacFormulas.phtByDecT(dt);
+      va = CardiacFormulas.mvaByPht(pht);
+    }
+    if (heartCycle.aPeakStart != DPoint.zero && heartCycle.aPeakStart != null) {
+      aDur = (heartCycle.diastoleEnd.x - heartCycle.aPeakStart!.x).abs();
+    }
+
+    return {
+      MeasureTerms.PS: ps,
+      MeasureTerms.ED: ed,
+      MeasureTerms.MD: md,
+      MeasureTerms.VelocityMax: pv,
+      MeasureTerms.AT: acctime,
+      MeasureTerms.Acceleration: accel,
+      MeasureTerms.TAMAX: taMax,
+      MeasureTerms.TAMEAN: taMean,
+      MeasureTerms.VTI: vti,
+      MeasureTerms.VTIMean: vtiMean,
+      MeasureTerms.MPG: mpg,
+      MeasureTerms.MMPG: mmpg,
+      MeasureTerms.EVEL: velE,
+      MeasureTerms.AVEL: velA,
+      MeasureTerms.EARatio: eAration,
+      MeasureTerms.DT: dt,
+      MeasureTerms.PHT: pht,
+      MeasureTerms.VA: va,
+      MeasureTerms.ADur: aDur,
+    };
+  }
+
+  double ratio(double d1, double d2) {
+    return d1 / d2;
+  }
+
+  Map<String, double> calculateDopplerTrace([
+    List<DPoint>? maxTraceLine,
+    double envStart = double.nan,
+    double envEnd = double.nan,
+    CardiacCycle? cardiacCycle,
+  ]) {
+    Map<String, double> baseValues = {};
+
+    if (cardiacCycle != null) {
+      CardiacCycle transitionCardiacCycle = CardiacCycle(
+        diastoleEnd: DPoint(0, 0),
+        systoleStart: DPoint(0, 0),
+        peakSystolic: DPoint(0, 0),
+        minimumAbsoluteVelocity: DPoint(0, 0),
+        index: 0,
+      );
+      transitionCardiacCycle.peakSystolic =
+          convertTimeMotionPoint(ref.feature!, cardiacCycle.peakSystolic);
+      transitionCardiacCycle.systoleStart =
+          convertTimeMotionPoint(ref.feature!, cardiacCycle.systoleStart);
+      transitionCardiacCycle.diastoleEnd =
+          convertTimeMotionPoint(ref.feature!, cardiacCycle.diastoleEnd);
+
+      transitionCardiacCycle.index = cardiacCycle.index;
+
+      cyclesStart = math.min(
+        transitionCardiacCycle.systoleStart.x,
+        cyclesStart,
+      );
+      cyclesEnd = math.min(
+        transitionCardiacCycle.diastoleEnd.x,
+        cyclesEnd,
+      );
+
+      List<DPoint> maxTraceLineOfCycle = maxTraceLine ?? [];
+      List<DPoint> meanTraceLineOfCycle =
+          maxTraceLine?.map((e) => DPoint(e.x, e.y * 0.75)).toList() ?? [];
+      baseValues = calculateHeartCycleRelevantValues(
+        transitionCardiacCycle,
+        maxTraceLineOfCycle,
+        meanTraceLineOfCycle,
+      );
+      double? ps = baseValues[MeasureTerms.PS];
+      double? ed = baseValues[MeasureTerms.ED];
+      double? md = baseValues[MeasureTerms.MD];
+      double? taMax = baseValues[MeasureTerms.TAMAX];
+
+      if (ps != null) {
+        if (ed != null) {
+          double maxPG = GeneralFormulas.maxPG(ps, ed);
+          double psed = ratio(ps, ed).abs();
+          double edps = ratio(ed, ps).abs();
+          double ri = GeneralFormulas.countRI(ps, ed);
+          double velocityMean = GeneralFormulas.medianVelocity(ps, ed);
+          baseValues.addAll({
+            MeasureTerms.MaxPG: maxPG,
+            MeasureTerms.PSED: psed,
+            MeasureTerms.EDPS: edps,
+            MeasureTerms.RI: ri.abs(),
+            MeasureTerms.VelocityMean: velocityMean,
+          });
+          if (taMax != null) {
+            double pi = GeneralFormulas.pi(ps, ed, taMax);
+            baseValues.addAll({
+              MeasureTerms.PI: pi,
+            });
+          }
+          double vMean = GeneralFormulas.medianVelocity(ps, ed);
+          double piTCD = GeneralFormulas.pi(ps, ed, vMean);
+          if (!piTCD.isNaN) {
+            baseValues.addAll({
+              MeasureTerms.PITCD: piTCD,
+            });
+          }
+        }
+        if (md != null) {
+          double rimd = GeneralFormulas.countRI(ps, md).abs();
+          baseValues.addAll({
+            MeasureTerms.RIMD: rimd,
+          });
+          if (taMax != null) {
+            double piMd = GeneralFormulas.pi(ps, md, taMax);
+            if (!piMd.isNaN) {
+              baseValues.addAll({
+                MeasureTerms.PIMD: piMd,
+              });
+            }
+          }
+        }
+      }
+      double? pv = baseValues[MeasureTerms.VelocityMax];
+
+      if (pv != null) {
+        double peakPG = GeneralFormulas.countPressure(pv);
+        baseValues.addAll({
+          MeasureTerms.PeakPG: peakPG,
+        });
+      }
+      double cycleEnvTimeSpan = (cyclesEnd - cyclesStart).abs();
+
+      if (cycleEnvTimeSpan > 0) {
+        /// dopplerTrace.AvgHeartCycle, 需要获取
+        double? hr = GeneralFormulas.countHR(cycleEnvTimeSpan);
+        baseValues.addAll({
+          MeasureTerms.HeartRate: hr,
+          MeasureTerms.TiEnv: cycleEnvTimeSpan,
+        });
+      }
+
+      double? velE = baseValues[MeasureTerms.EVEL];
+      double? velA = baseValues[MeasureTerms.AVEL];
+      if (velA != null && velE != null) {
+        double? eARatio = ratio(velA, velE).abs();
+        baseValues.addAll({
+          MeasureTerms.EARatio: eARatio,
+        });
+      }
+    } else if (!envStart.isNaN &&
+        !envEnd.isNaN &&
+        !envStart.almostEquals(double.minPositive, 0.000001) &&
+        !envEnd.almostEquals(double.minPositive, 0.000001)) {
+      // List<DPoint> maxTraceLine = [];
+      // List<DPoint> meanTraceLine = [];
+      Map<String, double> meanValues = tryCalculateByTrace(
+        maxTraceLine!.map((e) => DPoint(e.x, e.y * 0.75)).toList(),
+        envStart,
+        envEnd,
+        false,
+      );
+      Map<String, double> maxValues = tryCalculateByTrace(
+        maxTraceLine,
+        envStart,
+        envEnd,
+        true,
+      );
+      baseValues.addAll(meanValues);
+      baseValues.addAll(maxValues);
+    }
+
+    double? at = baseValues[MeasureTerms.AT];
+    double? dt = baseValues[MeasureTerms.DT];
+    double? et = baseValues[MeasureTerms.TiEnv];
+
+    if (at != null && dt != null && et != null) {
+      double? atdt = ratio(at, dt).abs();
+      double? atet = ratio(at, et).abs();
+      baseValues.addAll({
+        MeasureTerms.ATDTRatio: atdt,
+        MeasureTerms.ATETRatio: atet,
+      });
+    }
+
+    return baseValues;
+  }
+
+  Map<String, double> tryCalculateByTrace(
+    List<DPoint> traceLine,
+    double envStart,
+    double envEnd,
+    bool isMax,
+  ) {
+    double vti = double.nan;
+    double tiEnv = double.nan;
+    double ta = double.nan;
+    double pv = double.nan;
+    double meanPG = double.nan;
+
+    if (traceLine.isNotEmpty) {
+      List<DPoint> line = [];
+      for (var point in traceLine) {
+        line.add(point);
+        // if (point.x.almostNotLessThan(point.x, envStart) &&
+        //     point.x.almostNotGreaterThan(point.x, envEnd)) {
+        //   line.add(point);
+        // }
+      }
+      if (line.isNotEmpty) {
+        List<double> vueResult = GeneralFormulas.countVTI(line);
+        vti = vueResult.first;
+        var isShowAbsValue = true; // 假设这是从某个服务获取的值
+        if (isShowAbsValue) {
+          ta = vueResult[2].abs();
+          pv = vueResult[3].abs();
+          tiEnv = vueResult[1];
+          meanPG = vueResult[4];
+        }
+      }
+    }
+    if (isMax) {
+      return {
+        MeasureTerms.TAMAX: ta,
+        MeasureTerms.VTI: vti,
+        MeasureTerms.MPG: meanPG,
+        MeasureTerms.TiEnv: tiEnv,
+        MeasureTerms.VelocityMax: pv
+      };
+    }
+    return {
+      MeasureTerms.TAMEAN: ta,
+      MeasureTerms.VTIMean: vti,
+      MeasureTerms.MMPG: meanPG,
+      MeasureTerms.TiEnv: tiEnv,
+      MeasureTerms.VelocityMax: pv,
+      MeasureTerms.PeakPG: pv,
+    };
+  }
+}
+
+extension DoubleExtensions on double {
+  bool almostEquals(double other, double precision) {
+    if (isNaN && other.isNaN) {
+      return true;
+    }
+    return (this - other).abs() <= precision.abs();
+  }
+
+  bool almostNotLessThan(double double1, double double2,
+      [double precision = 0.000001]) {
+    if (double1 < double2) {
+      return true;
+    }
+    return double1.almostEquals(double2, precision);
+  }
+
+  bool almostNotGreaterThan(double double1, double double2,
+      [double precision = 0.000001]) {
+    if (double1 > double2) {
+      return true;
+    }
+    return double1.almostEquals(double2, precision);
+  }
+}

+ 84 - 102
lib/process/calcuators/semiauto_trace.dart

@@ -1,9 +1,7 @@
-import 'dart:ui';
+// ignore_for_file: invalid_use_of_protected_member
 
 import 'package:fis_measure/interfaces/date_types/point.dart';
-import 'package:fis_measure/interfaces/date_types/vector.dart';
 import 'package:fis_measure/interfaces/process/items/terms.dart';
-import 'package:fis_measure/interfaces/process/workspace/application.dart';
 import 'package:fis_measure/process/calcuators/formulas/cardiac.dart';
 import 'package:fis_measure/process/calcuators/formulas/general.dart';
 import 'package:fis_measure/process/items/item_feature.dart';
@@ -11,13 +9,12 @@ import 'package:fis_measure/process/primitives/multi_method/dop_trace_disp/cardi
 import 'package:fis_measure/process/primitives/multi_method/dop_trace_disp/data.dart';
 import 'dart:math' as math;
 import 'package:fis_measure/process/primitives/multi_method/semiauto_trace.dart';
-import 'package:get/get.dart';
 import 'calculator.dart';
 
 class SemiautoTraceCal extends Calculator<TraceItemAbstract, double> {
   SemiautoTraceCal(TraceItemAbstract ref) : super(ref);
   double cyclesStart = double.maxFinite;
-  double cyclesEnd = double.minPositive;
+  double cyclesEnd = 0;
   int validSystoleCyclesLength = 0;
 
   @override
@@ -26,54 +23,51 @@ class SemiautoTraceCal extends Calculator<TraceItemAbstract, double> {
 
     Map<String, double> calculateDopplerTraceResult = {};
 
-    final feature = ref.feature!;
-    final viewport = feature.hostVisualArea!.viewport!;
-
-    Size displaySize = Get.find<IApplication>().displaySize;
-    // 加入画布偏移量
-    final canvasOffset = feature.hostVisualArea!.layoutRegion!.topLeft;
-    //加入坐标系偏移量
-    final coordinateOffset = viewport.region;
-
-    var data = TraceListData.instance;
+    SemiautoTraceFeature feature = ref.feature! as SemiautoTraceFeature;
 
     /// 最大点集
-    List<DPoint> maxPhysicalPonints = data.maxPonints;
+    List<DPoint> maxPhysicalPonints = TraceListData.aboveMaxPonints;
+
+    List<CardiacCycle> validSystoleCycles = feature.currentCardiacCycleList;
 
-    List<CardiacCycle> validSystoleCycles = ref.currentCardiacCycleList;
     if (validSystoleCycles.isEmpty) {
-      final regionPoints = feature.innerPoints
-          .map((e) => viewport
-              .convert(e
-                  .clone()
-                  .addVector(DVector(-canvasOffset.x, -canvasOffset.y)))
-              .addVector(DVector(coordinateOffset.left, -coordinateOffset.top)))
+      List<DPoint> regionPoints = feature.innerPoints
+          .map((e) => convertTimeMotionPoint(feature, e))
           .toList();
-      final yFlippedPoints =
-          regionPoints.map((e) => DPoint(e.x, -e.y)).toList();
+      final yFlippedPoints = regionPoints;
       double min = math.min(yFlippedPoints.first.x, yFlippedPoints.last.x);
       double max = math.max(yFlippedPoints.first.x, yFlippedPoints.last.x);
-      calculateDopplerTraceResult =
-          calculateDopplerTrace(yFlippedPoints, min, max);
+      calculateDopplerTraceResult = calculateDopplerTrace(
+        yFlippedPoints,
+        min,
+        max,
+      );
     }
 
     if (validSystoleCyclesLength != validSystoleCycles.length) {
-      for (CardiacCycle i in validSystoleCycles) {
-        List<DPoint> regionPoints =
-            getPoints(maxPhysicalPonints, i, displaySize);
+      print(validSystoleCyclesLength);
+      for (int i = 0; i < validSystoleCycles.length; i++) {
+        /// 获取物理点坐标
+        List<DPoint> regionPoints = getPoints(
+          maxPhysicalPonints,
+          validSystoleCycles[i],
+        );
         final points = regionPoints.map((e) {
-          final currentPoint = viewport
-              .convert(DPoint(e.x / displaySize.width, e.y / displaySize.height)
-                  .clone()
-                  .addVector(DVector(-canvasOffset.x, -canvasOffset.y)))
-              .addVector(DVector(coordinateOffset.left, -coordinateOffset.top));
-          return DPoint(currentPoint.x, -currentPoint.y);
+          final currentPoint = convertTimeMotionPoint(feature, e);
+          return DPoint(currentPoint.x, currentPoint.y);
         }).toList();
+        if (points.isEmpty) {
+          return;
+        }
         double min = math.min(points.first.x, points.last.x);
         double max = math.max(points.first.x, points.last.x);
 
-        calculateDopplerTraceResult =
-            calculateDopplerTrace(points, min, max, i);
+        calculateDopplerTraceResult = calculateDopplerTrace(
+          points,
+          min,
+          max,
+          validSystoleCycles[i],
+        );
       }
 
       if (validSystoleCycles.isEmpty) {
@@ -85,6 +79,9 @@ class SemiautoTraceCal extends Calculator<TraceItemAbstract, double> {
     for (var output in ref.meta.outputs) {
       ///TODO:[Gavin] 实现以下计算逻辑
       switch (output.name) {
+        case MeasureTerms.Placeholder:
+          feature.updateStringValue(output, "", output.unit);
+          break;
         case MeasureTerms.TAMAX:
           if (calculateDopplerTraceResult[MeasureTerms.TAMAX] != null) {
             feature.updateFloatValue(output,
@@ -304,61 +301,31 @@ class SemiautoTraceCal extends Calculator<TraceItemAbstract, double> {
     }
   }
 
-  DPoint transitionPoint(DPoint i) {
-    final feature = ref.feature!;
-    final viewport = feature.hostVisualArea!.viewport!;
-    // RectRegion? region = feature.hostVisualArea!.layoutRegion;
-
-    Size displaySize = Get.find<IApplication>().displaySize;
-    final widthScale = Get.find<IApplication>().displayScaleRatio;
-    // 加入画布偏移量
-    final canvasOffset = feature.hostVisualArea!.layoutRegion!.topLeft;
-    //加入坐标系偏移量
-    final coordinateOffset = viewport.region;
-    return viewport
-        .convert(DPoint(i.clone().x / displaySize.width * widthScale,
-                i.clone().y / displaySize.height * widthScale)
-            .clone()
-            .addVector(DVector(-canvasOffset.x, -canvasOffset.y)))
-        .addVector(DVector(coordinateOffset.left, -coordinateOffset.top));
-  }
-
-  List<DPoint> getPoints(List<DPoint> lines, CardiacCycle cycle, Size size) {
+  List<DPoint> getPoints(
+    List<DPoint> lines,
+    CardiacCycle cycle,
+  ) {
+    // Size displaySize = Get.find<IApplication>().displaySize;
     List<DPoint> points = [];
     for (DPoint line in lines) {
-      DPoint point = DPoint(line.x, line.y);
-      if (point.x >= cycle.systoleStart.x && point.x <= cycle.diastoleEnd.x) {
-        points.add(DPoint(point.x / size.width, point.y / size.height));
+      // DPoint logicPoint = convert2LogicPoint(line);
+      final systoleStartX = cycle.systoleStart.x;
+      final diastoleEndX = cycle.diastoleEnd.x;
+      if (line.x >= systoleStartX && line.x <= diastoleEndX) {
+        points.add(DPoint(line.x, line.y));
       }
     }
     return points;
   }
 
-  /// 转成物理坐标
-  DPoint convert2LogicPoint(Size size, DPoint viewPoint, double baseLine,
-      double width, double height) {
-    final x = viewPoint.x / size.width * width;
-    final y = viewPoint.y - (baseLine * size.height) * height;
-    return DPoint(x, y);
-  }
-
-  static List<DPoint> getFeatureYFlippedPoints(MeasureItemFeature feature) {
-    final viewport = feature.hostVisualArea!.viewport!;
-    // 加入画布偏移量
-    final canvasOffset = feature.hostVisualArea!.layoutRegion!.topLeft;
-    //加入坐标系偏移量
-    final coordinateOffset = viewport.region;
+  List<DPoint> getFeatureYFlippedPoints(MeasureItemFeature feature) {
     final regionPoints = feature.innerPoints
-        .map((e) => viewport
-            .convert(
-                e.clone().addVector(DVector(-canvasOffset.x, -canvasOffset.y)))
-            .addVector(DVector(coordinateOffset.left, -coordinateOffset.top)))
+        .map((e) => convertTimeMotionPoint(feature, e))
         .toList();
-    final points = regionPoints.map((e) => DPoint(e.x, -e.y)).toList();
-    return points;
+    return regionPoints;
   }
 
-  static List<double> getCountVTI(MeasureItemFeature feature) {
+  List<double> getCountVTI(MeasureItemFeature feature) {
     final yFlippedPoints = getFeatureYFlippedPoints(feature);
     final result = GeneralFormulas.countVTI(yFlippedPoints);
     return result;
@@ -375,7 +342,9 @@ class SemiautoTraceCal extends Calculator<TraceItemAbstract, double> {
     double taMean = double.nan;
     double mmpg = double.nan;
     if (meanTraceLineOfCycle.isNotEmpty) {
-      vtiMean = GeneralFormulas.countVTI(meanTraceLineOfCycle).first;
+      List<double> meanResult = GeneralFormulas.countVTI(meanTraceLineOfCycle);
+      vtiMean = meanResult.first;
+      taMean = meanResult[2];
     }
 
     double vti = double.nan;
@@ -479,22 +448,27 @@ class SemiautoTraceCal extends Calculator<TraceItemAbstract, double> {
         minimumAbsoluteVelocity: DPoint(0, 0),
         index: 0,
       );
-      transitionCardiacCycle.peakSystolic = DPoint(
-          transitionPoint(cardiacCycle.peakSystolic).x,
-          -transitionPoint(cardiacCycle.peakSystolic).y);
-      transitionCardiacCycle.systoleStart = DPoint(
-          transitionPoint(cardiacCycle.systoleStart).x,
-          -transitionPoint(cardiacCycle.systoleStart).y);
-      transitionCardiacCycle.diastoleEnd = DPoint(
-          transitionPoint(cardiacCycle.diastoleEnd).x,
-          -transitionPoint(cardiacCycle.diastoleEnd).y);
-
-      cyclesStart =
-          math.min(transitionCardiacCycle.systoleStart.x, cyclesStart);
-      cyclesEnd = math.max(transitionCardiacCycle.diastoleEnd.x, cyclesEnd);
+      transitionCardiacCycle.peakSystolic =
+          convertTimeMotionPoint(ref.feature!, cardiacCycle.peakSystolic);
+      transitionCardiacCycle.systoleStart =
+          convertTimeMotionPoint(ref.feature!, cardiacCycle.systoleStart);
+      transitionCardiacCycle.diastoleEnd =
+          convertTimeMotionPoint(ref.feature!, cardiacCycle.diastoleEnd);
+
+      transitionCardiacCycle.index = cardiacCycle.index;
+
+      cyclesStart = math.min(
+        transitionCardiacCycle.systoleStart.x,
+        cyclesStart,
+      );
+      cyclesEnd = math.min(
+        transitionCardiacCycle.diastoleEnd.x,
+        cyclesEnd,
+      );
 
       List<DPoint> maxTraceLineOfCycle = maxTraceLine ?? [];
-      List<DPoint> meanTraceLineOfCycle = maxTraceLine ?? [];
+      List<DPoint> meanTraceLineOfCycle =
+          maxTraceLine?.map((e) => DPoint(e.x, e.y * 0.75)).toList() ?? [];
       baseValues = calculateHeartCycleRelevantValues(
         transitionCardiacCycle,
         maxTraceLineOfCycle,
@@ -580,11 +554,19 @@ class SemiautoTraceCal extends Calculator<TraceItemAbstract, double> {
         !envStart.almostEquals(double.minPositive, 0.000001) &&
         !envEnd.almostEquals(double.minPositive, 0.000001)) {
       // List<DPoint> maxTraceLine = [];
-      List<DPoint> meanTraceLine = [];
-      Map<String, double> meanValues =
-          tryCalculateByTrace(meanTraceLine, envStart, envEnd, false);
-      Map<String, double> maxValues =
-          tryCalculateByTrace(maxTraceLine!, envStart, envEnd, true);
+      // List<DPoint> meanTraceLine = [];
+      Map<String, double> meanValues = tryCalculateByTrace(
+        maxTraceLine!.map((e) => DPoint(e.x, e.y * 0.75)).toList(),
+        envStart,
+        envEnd,
+        false,
+      );
+      Map<String, double> maxValues = tryCalculateByTrace(
+        maxTraceLine,
+        envStart,
+        envEnd,
+        true,
+      );
       baseValues.addAll(meanValues);
       baseValues.addAll(maxValues);
     }

+ 6 - 0
lib/process/items/factory.dart

@@ -19,6 +19,7 @@ import 'package:fis_measure/process/primitives/combos/two_location.dart';
 import 'package:fis_measure/process/primitives/combos/two_ray.dart';
 import 'package:fis_measure/process/primitives/combos/two_straightline.dart';
 import 'package:fis_measure/process/primitives/combos/two_sv.dart';
+import 'package:fis_measure/process/primitives/multi_method/auto_doppler_trace.dart';
 import 'package:fis_measure/process/primitives/urm_measure/urm_den.dart';
 import 'package:fis_measure/process/primitives/urm_measure/urm_trace_measure.dart';
 import 'package:fis_measure/process/primitives/urm_measure/urm_ab_ratio.dart';
@@ -381,5 +382,10 @@ class MeasureItemFactory {
       MeasureTypes.SemiautoTrace,
       SemiautoTrace.createTrace,
     );
+
+    _singleton._register(
+      MeasureTypes.AutoDopplerTrace,
+      AutoDopplerTrace.createTrace,
+    );
   }
 }

+ 12 - 0
lib/process/items/item_meta_convert.dart

@@ -13,6 +13,8 @@ class ItemMetaConverter {
 
   late final List<ItemMeta> _childItems = [];
 
+  late final List<ItemMeta> _multiMethodItems = [];
+
   int? _rimWidth;
 
   /// 输出测量项元
@@ -35,6 +37,7 @@ class ItemMetaConverter {
       briefAnnotation: dto.briefAnnotation ?? '',
       outputs: _outputs,
       childItems: _childItems,
+      multiMethodItems: _multiMethodItems,
 
       /// 【TODO】 暂时屏蔽 buyStatus
       // buyStatus: dto.status,
@@ -56,6 +59,13 @@ class ItemMetaConverter {
     }
   }
 
+  /// 多测量方法
+  void _loadMultiMethodItems() {
+    for (var child in dto.multiMethodItems!) {
+      _multiMethodItems.add(_convertChildItem(child));
+    }
+  }
+
   /// 多测量方法
   void _loadMulti() {
     final workingItem = dto.multiMethodItems!.firstWhere((e) => e.isWorking);
@@ -69,6 +79,8 @@ class ItemMetaConverter {
     if (workingItem.rimWidth != null) {
       _rimWidth = workingItem.rimWidth;
     }
+    _loadMultiMethodItems();
+
     _outputs = _convetOutputsFromCalc(workingItem.calculator!);
   }
 

+ 430 - 0
lib/process/primitives/multi_method/auto_doppler_trace.dart

@@ -0,0 +1,430 @@
+// ignore_for_file: invalid_use_of_protected_member
+
+import 'dart:ui';
+
+import 'package:fis_measure/interfaces/date_types/point.dart';
+import 'package:fis_measure/interfaces/enums/items.dart';
+import 'package:fis_measure/interfaces/process/items/item.dart';
+import 'package:fis_measure/interfaces/process/items/item_metas.dart';
+import 'package:fis_measure/interfaces/process/workspace/point_info.dart';
+import 'package:fis_measure/process/calcuators/auto_doppler_trace.dart';
+import 'package:fis_measure/process/items/item.dart';
+import 'package:fis_measure/process/items/item_feature.dart';
+import 'package:fis_measure/process/physical_coordinates/doppler.dart';
+import 'package:fis_measure/process/primitives/multi_method/dop_trace_disp/cardiac_cycle.dart';
+import 'package:fis_measure/process/primitives/multi_method/dop_trace_disp/data.dart';
+import 'package:fis_measure/utils/canvas.dart';
+import 'package:fis_measure/utils/prompt_box.dart';
+import 'package:fis_measure/view/gesture/positioned_touch_cursor.dart';
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+
+class AutoDopplerTrace extends TraceItemAbstract {
+  AutoDopplerTrace(ItemMeta meta, IMeasureItem? parent) : super(meta, parent);
+  late final touchState = Get.find<ITouchPointState>();
+
+  @override
+  bool onExecuteMouse(PointInfo args) {
+    if (state == ItemStates.finished) {
+      if (args.pointType == PointInfoType.mouseDown) {
+        state = ItemStates.waiting;
+      }
+    }
+
+    if (state == ItemStates.waiting) {
+      if (args.pointType == PointInfoType.mouseDown) {
+        handleMouseDownWhileWaiting(args);
+
+        handleFinish();
+      } else if (state == ItemStates.running) {
+        if (args.pointType == PointInfoType.mouseUp) return false;
+      }
+    }
+    return true;
+  }
+
+  void handleFinish() async {
+    await Future.delayed(const Duration(milliseconds: 50));
+    List<CardiacCycle> currentCardiacCycleList =
+        feature?.refItem.currentCardiacCycleList ?? [];
+    print(currentCardiacCycleList.length);
+    if (currentCardiacCycleList.isNotEmpty) {
+      feature?.refItem.setInnerPoints([
+        currentCardiacCycleList.first.systoleStart,
+        currentCardiacCycleList.last.diastoleEnd
+      ]);
+    }
+    feature?.refItem.setCurrentCardiacCycleList([]);
+    feature!.isActive = false;
+    doCalculate();
+    doFeatureFinish();
+    doFeatureUpdate(); // 若不执行,子测量将无法自动切换
+    PromptBox.dismiss();
+  }
+
+  @override
+  void doFeatureFinish() {
+    super.doFeatureFinish();
+  }
+
+  void synchToMainMonitorScreen(PointInfo args) {
+    final point = args.toAreaLogicPoint();
+    var x = point.x;
+    var y = point.y;
+  }
+
+  void handleMouseDownWhileWaiting(PointInfo args) {
+    // TODO: 判断是否当前area
+    // 转换为Area逻辑位置
+    feature = AutoDopplerTraceFeature(this);
+    if (args.hostVisualArea != null) {
+      feature!.hostVisualArea = args.hostVisualArea;
+    }
+    final point = args.toAreaLogicPoint();
+    feature!.adopt(point);
+    state = ItemStates.running;
+  }
+
+  void handleTouchDownWhileWaiting(PointInfo args) {
+    print("画结束了");
+    // TODO: 判断是否当前area
+    // 转换为Area逻辑位置
+    feature = AutoDopplerTraceFeature(this);
+    if (args.hostVisualArea != null) {
+      feature!.hostVisualArea = args.hostVisualArea;
+    }
+    final point = args.toAreaLogicPoint();
+    feature!.adopt(point);
+    // state = ItemStates.running;
+  }
+
+  PointInfo? startPoint;
+  DPoint touchStartPosition = DPoint(0, 0); // 相对位移起始触摸点
+  bool isFirstPointMove = false;
+  @override
+  bool onExecuteTouch(PointInfo args) {
+    if (state == ItemStates.finished) {
+      if (args.pointType == PointInfoType.touchDown) {
+        state = ItemStates.waiting;
+      }
+    }
+
+    if (state == ItemStates.waiting) {
+      if (isFirstPointMove) {
+        args.addOffset(0, -0.2);
+      }
+      switch (args.pointType) {
+        case PointInfoType.touchDown:
+          isFirstPointMove = false;
+          startPoint = args; // 设置线段起点
+          handleTouchDownWhileWaiting(startPoint!); // 通过设置的起点开始一个绘制事件
+          break;
+        case PointInfoType.touchUp:
+          startPoint = args; // 设置线段起点
+          state = ItemStates.running;
+          touchState.touchOffset = Offset.zero;
+          break; // 按下立即抬起无事发生
+        case PointInfoType.touchMove:
+          if (isMoveTargetOutOfRange(args)) return true;
+          isFirstPointMove = true;
+          final pixelSize = application.displaySize;
+          touchState.touchOffset =
+              DPoint(0, -0.2).scale2Size(pixelSize).toOffset();
+          feature?.innerPoints.first = args;
+          break;
+        default:
+          break;
+      }
+    } else if (state == ItemStates.running) {
+      if (args.pointType == PointInfoType.touchDown) {
+        touchStartPosition = args;
+        final pixelSize = application.displaySize;
+        touchState.touchOffset = startPoint!.scale2Size(pixelSize).toOffset() -
+            args.scale2Size(pixelSize).toOffset();
+      }
+      if (args.pointType == PointInfoType.touchUp) {
+        touchState.touchOffset = Offset.zero;
+        doFeatureFinish();
+      }
+      if (args.pointType == PointInfoType.touchMove) {
+        PointInfo newPoint = PointInfo.fromOffset(
+            startPoint!.clone().addVector(args - touchStartPosition).toOffset(),
+            startPoint!.pointType);
+        if (isMoveTargetOutOfRange(newPoint)) return true;
+        feature?.adopt(newPoint);
+        doCalculate();
+      }
+    }
+    return true;
+  }
+
+  static AutoDopplerTrace createTrace(
+    ItemMeta meta, [
+    IMeasureItem? parent,
+  ]) {
+    AutoDopplerTrace trace = AutoDopplerTrace(meta, parent);
+    trace.calculator = AutoDopplerTraceCal(trace);
+    return trace;
+  }
+}
+
+class AutoDopplerTraceFeature extends AutoDopplerTraceItemFeatureAbstract {
+  AutoDopplerTraceFeature(TraceItemAbstract refItem) : super(refItem);
+  final greenPen = Paint()
+    ..color = const Color.fromARGB(255, 0, 255, 0)
+    ..isAntiAlias = true
+    ..strokeWidth = 1
+    ..style = PaintingStyle.stroke;
+  final dashLinePan = Paint()
+    ..color = const Color.fromARGB(255, 255, 255, 0)
+    ..isAntiAlias = false
+    ..strokeWidth = 1
+    ..style = PaintingStyle.stroke;
+
+  DPoint furthestPoint = DPoint(0, 0);
+
+  List<CardiacCycle> currentCardiacCycleList = [];
+  late Path path;
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    final double areaTop = hostVisualArea!.displayRegion.top * size.height;
+    final double areaBottom =
+        hostVisualArea!.displayRegion.bottom * size.height;
+    if (innerPoints.isEmpty) return;
+    // if (innerPoints.length == 1) {
+    //   // drawVertex(canvas, convert2ViewPoint(size, innerPoints[0]).toOffset());
+    //   // drawId(canvas, size);
+    //   // return;
+    // }
+    final points = innerPoints.map((e) => convert2ViewPoint(size, e)).toList();
+    final startOffset = convert2ViewPoint(size, startPoint);
+    final endOffset = convert2ViewPoint(size, endPoint);
+
+    double baseLine =
+        (hostVisualArea!.viewport!.physical as DopplerPhysicalCoordinate)
+            .baseLine;
+    double pwHeight = size.height * hostVisualArea!.layoutRegion!.height;
+
+    double baseLineHeight = areaTop + baseLine * pwHeight;
+
+    // TraceListData data = TraceListData();
+    List<DPoint> maxPonints = [];
+    List<DPoint> logicalPoints = [];
+
+    /// 当前周期数据
+    List<CardiacCycle> cardiacCycleList = [];
+    List<CardiacCycle> logicalCardiacCycleList = [];
+    List<BestCardiacCycle> logicalBestCardiacCycleList = [];
+    path = Path();
+
+    /// 最大点集
+    /// below: 1
+    /// above: 2
+    if (startOffset.y < baseLineHeight) {
+      // data.setLocation("above");
+      logicalPoints = List.generate(
+        TraceListData.aboveMaxPonints.length,
+        (index) => TraceListData.aboveMaxPonints[index],
+      );
+      logicalCardiacCycleList = List.generate(
+        TraceListData.aboveCardiacCycleList.length,
+        (index) => TraceListData.aboveCardiacCycleList[index],
+      );
+      logicalBestCardiacCycleList = List.generate(
+        TraceListData.aboveOptimumCardiacCycleList.length,
+        (index) => TraceListData.aboveOptimumCardiacCycleList[index],
+      );
+    } else {
+      logicalPoints = List.generate(
+        TraceListData.belowMaxPonints.length,
+        (index) => TraceListData.belowMaxPonints[index],
+      );
+      logicalCardiacCycleList = List.generate(
+        TraceListData.belowCardiacCycleList.length,
+        (index) => TraceListData.belowCardiacCycleList[index],
+      );
+      logicalBestCardiacCycleList = List.generate(
+        TraceListData.belowOptimumCardiacCycleList.length,
+        (index) => TraceListData.belowOptimumCardiacCycleList[index],
+      );
+    }
+
+    maxPonints = logicalPoints.map((e) => convert2ViewPoint(size, e)).toList();
+    for (int i = 0; i < logicalCardiacCycleList.length; i++) {
+      logicalCardiacCycleList[i].index = i;
+      cardiacCycleList.add(logicalCardiacCycleList[i]);
+    }
+
+    /// 这是正在绘制的
+    if (points.isNotEmpty) {
+      /// 这边自动测量的最佳周期先默认选择3 后面需要配置
+      if (logicalBestCardiacCycleList.length > 3) {
+        if (logicalBestCardiacCycleList[2] == null) {
+          return;
+        }
+        DPoint logicalBestCardiacCycleStartDPoint = convert2ViewPoint(
+            size, logicalBestCardiacCycleList[2].bestSystoleStart);
+        DPoint logicalBestCardiacCycleEndDPoint = convert2ViewPoint(
+            size, logicalBestCardiacCycleList[2].bestDiastoleEnd);
+        // 绘制路径
+        final path = Path()
+          ..moveTo(logicalBestCardiacCycleStartDPoint.x,
+              logicalBestCardiacCycleStartDPoint.y);
+
+        for (var point in maxPonints) {
+          if (point.x >= logicalBestCardiacCycleStartDPoint.x &&
+              point.x <= logicalBestCardiacCycleEndDPoint.x) {
+            path.lineTo(point.x, point.y);
+          }
+        }
+
+        canvas.drawPath(path, greenPen);
+
+        /// 根据周期绘制
+        for (CardiacCycle i in cardiacCycleList) {
+          DPoint diastoleEnd = convert2ViewPoint(size, i.diastoleEnd);
+          DPoint systoleStart = convert2ViewPoint(size, i.systoleStart);
+          DPoint ePeak = convert2ViewPoint(size, i.ePeak ?? DPoint(0, 0));
+
+          /// ai提供的周期数据可能不对
+          if (systoleStart.x <= diastoleEnd.x &&
+              systoleStart.x >= logicalBestCardiacCycleStartDPoint.x &&
+              diastoleEnd.x <= logicalBestCardiacCycleEndDPoint.x) {
+            canvas.drawDashLine(
+              Offset(systoleStart.x, areaTop),
+              Offset(systoleStart.x, areaBottom),
+              3,
+              3,
+              dashLinePan,
+            );
+            canvas.drawDashLine(
+              Offset(diastoleEnd.x, areaTop),
+              Offset(diastoleEnd.x, areaBottom),
+              3,
+              3,
+              dashLinePan,
+            );
+            if (ePeak.x < endOffset.x) {
+              drawCrossVertex(
+                canvas,
+                ePeak.toOffset(),
+              );
+            }
+
+            currentCardiacCycleList.add(i);
+          }
+        }
+      }
+
+      refItem
+          .setCurrentCardiacCycleList(currentCardiacCycleList.toSet().toList());
+
+      canvas.drawPath(
+        path,
+        greenPen,
+      );
+    }
+  }
+
+  /// 转成物理坐标
+  DPoint convert2LogicPoint(
+    Size size,
+    DPoint viewPoint,
+    double baseLine,
+    double width,
+    double height,
+  ) {
+    final x = viewPoint.x / size.width * width;
+    final y = viewPoint.y - (baseLine * size.height) * height;
+    return DPoint(x, y);
+  }
+}
+
+abstract class AutoDopplerTraceItemFeatureAbstract extends MeasureItemFeature {
+  AutoDopplerTraceItemFeatureAbstract(TraceItemAbstract refItem)
+      : super(refItem);
+
+  @override
+  TraceItemAbstract get refItem => super.refItem as TraceItemAbstract;
+
+  DPoint get startPoint => innerPoints.first;
+  DPoint get endPoint => innerPoints.last;
+
+  bool ifRightSide = true;
+
+  /// 接收新坐标
+  void adopt(DPoint point) {
+    if (innerPoints.isEmpty) {
+      innerPoints.add(point);
+    }
+    if (point.x > startPoint.x) {
+      handleChangeSide(point);
+      if (point.x < innerPoints.last.x) {
+        clearRight(point.x);
+      } else if (point.x > innerPoints.last.x) {
+        innerPoints.add(point);
+      }
+    } else {
+      handleChangeSide(point);
+      if (point.x > innerPoints.last.x) {
+        clearLeft(point.x);
+      } else if (point.x < innerPoints.last.x) {
+        innerPoints.add(point);
+      }
+    }
+  }
+
+  void clearRight(double X) {
+    if (innerPoints.isEmpty) return;
+    for (var i = innerPoints.length - 1; i >= 0; i--) {
+      if (innerPoints[i].x >= X) {
+        innerPoints.removeAt(i);
+      }
+    }
+  }
+
+  void clearLeft(double X) {
+    if (innerPoints.isEmpty) return;
+    for (var i = innerPoints.length - 1; i >= 0; i--) {
+      if (innerPoints[i].x <= X) {
+        innerPoints.removeAt(i);
+      }
+    }
+  }
+
+  void handleChangeSide(point) {
+    if (ifRightSide) {
+      if (point.x < startPoint.x) {
+        ifRightSide = false;
+        innerPoints.clear();
+        innerPoints.add(point);
+      }
+    } else {
+      if (point.x > startPoint.x) {
+        ifRightSide = true;
+        innerPoints.clear();
+        innerPoints.add(point);
+      }
+    }
+  }
+}
+
+abstract class TraceItemAbstract
+    extends MeasureItem<AutoDopplerTraceItemFeatureAbstract> {
+  TraceItemAbstract(ItemMeta meta, IMeasureItem? parent) : super(meta, parent);
+
+  List<CardiacCycle> currentCardiacCycleList = [];
+  void setCurrentCardiacCycleList(List<CardiacCycle> _currentCardiacCycleList) {
+    currentCardiacCycleList = _currentCardiacCycleList;
+  }
+
+  void setInnerPoints(List<DPoint> points) {
+    feature!.innerPoints.clear();
+    feature!.innerPoints.addAll(points);
+  }
+
+  void clearInnerPoints() {
+    feature!.innerPoints.clear();
+  }
+}

+ 22 - 0
lib/process/primitives/multi_method/dop_trace_disp/cardiac_cycle.dart

@@ -1,15 +1,24 @@
 import 'package:fis_measure/interfaces/date_types/point.dart';
 
 class CardiacCycle {
+  // 收缩期起点
   late DPoint systoleStart;
+  // 舒张期终点
   late DPoint diastoleEnd;
+  // 收缩期峰值
   late DPoint peakSystolic;
+  // MD
   late DPoint minimumAbsoluteVelocity;
   late int index;
+  // 所在频谱段
   late int regionId;
+  // E峰终点
   late DPoint? ePeakEnd;
+  // A峰起点
   late DPoint? aPeakStart;
+  // E峰
   late DPoint? ePeak;
+  // A峰
   late DPoint? aPeak;
 
   CardiacCycle({
@@ -25,3 +34,16 @@ class CardiacCycle {
     this.aPeak,
   });
 }
+
+/// 最佳心动周期
+class BestCardiacCycle {
+  // 最佳收缩期起点
+  late DPoint bestSystoleStart;
+  // 最佳舒张期终点
+  late DPoint bestDiastoleEnd;
+
+  BestCardiacCycle({
+    required this.bestSystoleStart,
+    required this.bestDiastoleEnd,
+  });
+}

+ 165 - 41
lib/process/primitives/multi_method/dop_trace_disp/data.dart

@@ -5,54 +5,160 @@ import 'package:fis_measure/process/primitives/multi_method/dop_trace_disp/cardi
 import 'package:get/get.dart';
 
 class TraceListData {
-  VetAutoTraceImageResult? _vetAutoTraceImageResult;
-  VetAutoTraceImageResult? get vetAutoTraceImageResult =>
+  static VetAutoTraceImageResult? _vetAutoTraceImageResult;
+
+  static VetAutoTraceImageResult? get vetAutoTraceImageResult =>
       _vetAutoTraceImageResult;
 
-  TraceListData._internal() {
-    setLocation(_currentType);
-    localData();
-    getCardiacCycles();
-  }
+  static String currentLocationName = "";
 
   static final List<DPoint> _maxPonints = [];
-  static List<CardiacCycle> _cardiacCycleList = [];
-
-  TraceLocationType _currentType = TraceLocationType.below;
-
-  static List<DPoint> belowList = [];
-  static List<DPoint> aboveList = [];
-  static List<DPoint> bothList = [];
+  static final List<CardiacCycle> _cardiacCycleList = [];
 
-  static List<DPoint> initList = [];
   List<DPoint> get maxPonints => _maxPonints;
 
-  List<CardiacCycle> get cardiacCycleList => _cardiacCycleList;
+  static List<CardiacCycle> get cardiacCycleList => _cardiacCycleList;
+
+  /// 点集
+  static List<DPoint> belowMaxPonints = [];
+  static List<DPoint> aboveMaxPonints = [];
+
+  /// 心动周期
+  static List<CardiacCycle> belowCardiacCycleList = [];
+  static List<CardiacCycle> aboveCardiacCycleList = [];
+
+  /// 最佳心动周期
+  static List<BestCardiacCycle> belowOptimumCardiacCycleList = [];
+  static List<BestCardiacCycle> aboveOptimumCardiacCycleList = [];
+
+  static TraceData? loadPonintsData(String locationName) {
+    List<DPoint> points = [];
+    List<PWTracePointDTO> contourList = [];
+    List<CardiacCycle> cardiacCycleList = [];
+    List<BestCardiacCycle> optimumCardiacCycleList = [];
+    Map<String, PWTraceDTO>? pWTraceInfos =
+        vetAutoTraceImageResult?.pWTraceInfos;
+    pWTraceInfos?.forEach((key, value) {
+      if (key == locationName) {
+        contourList = value.contour ?? [];
+        cardiacCycleList = getCardiacCycles(
+          value.cycleInfos,
+          value.contour,
+        );
+        optimumCardiacCycleList = getOptimumCardiacCycles(
+          value.contour,
+          value.bestCycles,
+        );
+      }
+    });
+
+    if (contourList.isEmpty) {
+      return null;
+    }
 
-  void setLocation(TraceLocationType type) {
-    aboveList = belowList;
-    if (type == TraceLocationType.above) {
-      initList = aboveList;
-    } else if (type == TraceLocationType.below) {
-      initList = belowList;
-    } else if (type == TraceLocationType.both) {
-      initList = bothList;
+    for (int i = 0; i < contourList.length; i++) {
+      points.add(
+        convert(
+          DPoint(
+            contourList[i].x.toDouble(),
+            contourList[i].y.toDouble(),
+          ),
+        ),
+      );
     }
-    _currentType = type;
+    return TraceData(
+      maxPonints: points,
+      cardiacCycle: cardiacCycleList,
+      bestCardiacCycle: optimumCardiacCycleList,
+    );
   }
 
-  static List<DPoint> localData() {
-    for (int i = 0; i < initList.length; i++) {
-      _maxPonints.add(convert(initList[i]));
+  /// 返回的数据很怪
+  static List<BestCardiacCycle> getOptimumCardiacCycles(
+    List<PWTracePointDTO>? contour,
+    PWTraceBestCycleDTO? bestCycles,
+  ) {
+    List<BestCardiacCycle> cycleLists = [];
+    if (bestCycles != null && contour != null) {
+      PWTraceBestCycleIndexDTO? oneBestCycle = bestCycles.oneBestCycle;
+      PWTraceBestCycleIndexDTO? twoBestCycle = bestCycles.twoBestCycle;
+      PWTraceBestCycleIndexDTO? threeBestCycle = bestCycles.threeBestCycle;
+      PWTraceBestCycleIndexDTO? fourBestCycle = bestCycles.fourBestCycle;
+      PWTraceBestCycleIndexDTO? fiveBestCycle = bestCycles.fiveBestCycle;
+      List<PWTraceBestCycleIndexDTO?> bestCycle = [
+        oneBestCycle,
+        twoBestCycle,
+        threeBestCycle,
+        fourBestCycle,
+        fiveBestCycle
+      ];
+      for (int i = 0; i < bestCycle.length; i++) {
+        if (bestCycle[i] != null) {
+          cycleLists.add(BestCardiacCycle(
+            bestDiastoleEnd: convert(
+              DPoint(
+                contour[bestCycle[i]?.bestEnd ?? 0].x.toDouble(),
+                contour[bestCycle[i]?.bestEnd ?? 0].y.toDouble(),
+              ),
+            ),
+            bestSystoleStart: convert(
+              DPoint(
+                contour[bestCycle[i]?.bestStart ?? 0].x.toDouble(),
+                contour[bestCycle[i]?.bestStart ?? 0].y.toDouble(),
+              ),
+            ),
+          ));
+        }
+      }
     }
-    return _maxPonints;
+
+    return cycleLists;
   }
 
-  List<CardiacCycle> getCardiacCycles() {
-    _cardiacCycleList = [];
-    return _cardiacCycleList;
+  static List<CardiacCycle> getCardiacCycles(
+      List<PWTraceCycleDTO>? pwTraceCycleLists,
+      List<PWTracePointDTO>? contour) {
+    List<CardiacCycle> cycleLists = [];
+    if (pwTraceCycleLists != null) {
+      for (int i = 0; i < pwTraceCycleLists.length; i++) {
+        int cycleEnd = pwTraceCycleLists[i].cycleEnd;
+        int cycleStart = pwTraceCycleLists[i].cycleStart;
+        int epeak = pwTraceCycleLists[i].epeak;
+        cycleLists.add(CardiacCycle(
+          minimumAbsoluteVelocity: DPoint(0, 0),
+          diastoleEnd: convert(
+            DPoint(
+              contour![cycleEnd].x.toDouble(),
+              contour[cycleEnd].y.toDouble(),
+            ),
+          ),
+          index: 0,
+          peakSystolic: convert(
+            DPoint(
+              contour[epeak].x.toDouble(),
+              contour[epeak].y.toDouble(),
+            ),
+          ),
+          ePeak: convert(
+            DPoint(
+              contour[epeak].x.toDouble(),
+              contour[epeak].y.toDouble(),
+            ),
+          ),
+          systoleStart: convert(
+            DPoint(
+              contour[cycleStart].x.toDouble(),
+              contour[cycleStart].y.toDouble(),
+            ),
+          ),
+        ));
+      }
+    }
+
+    return cycleLists;
   }
 
+  /// 转化成逻辑点坐标
   static DPoint convert(
     DPoint point,
   ) {
@@ -60,21 +166,39 @@ class TraceListData {
     final x = point.x;
     final y = point.y;
     final widthScale = Get.find<IApplication>().displayScaleRatio;
+    final displaySize = Get.find<IApplication>().displaySize;
 
-    return DPoint(x * widthScale, y * widthScale);
+    return DPoint(
+      (x * widthScale) / displaySize.width,
+      (y * widthScale) / displaySize.height,
+    );
   }
 
-  static final TraceListData _instance = TraceListData._internal();
+  /// 设置自动/半自动数据
+  static void setVetAutoTraceImageResult(VetAutoTraceImageResult? result) {
+    _vetAutoTraceImageResult = result;
+
+    belowMaxPonints = loadPonintsData("below")?.maxPonints ?? [];
+    aboveMaxPonints = loadPonintsData("above")?.maxPonints ?? [];
 
-  static TraceListData get instance => _instance;
+    belowCardiacCycleList = loadPonintsData("below")?.cardiacCycle ?? [];
+    aboveCardiacCycleList = loadPonintsData("above")?.cardiacCycle ?? [];
 
-  void setVetAutoTraceImageResult(VetAutoTraceImageResult? result) {
-    _vetAutoTraceImageResult = result;
+    belowOptimumCardiacCycleList =
+        loadPonintsData("below")?.bestCardiacCycle ?? [];
+    aboveOptimumCardiacCycleList =
+        loadPonintsData("above")?.bestCardiacCycle ?? [];
   }
 }
 
-enum TraceLocationType {
-  below,
-  above,
-  both,
+class TraceData {
+  List<DPoint> maxPonints;
+  List<CardiacCycle> cardiacCycle;
+  List<BestCardiacCycle> bestCardiacCycle;
+
+  TraceData({
+    required this.maxPonints,
+    required this.cardiacCycle,
+    required this.bestCardiacCycle,
+  });
 }

+ 117 - 147
lib/process/primitives/multi_method/semiauto_trace.dart

@@ -1,11 +1,9 @@
-import 'dart:ui';
+// ignore_for_file: invalid_use_of_protected_member
 
 import 'package:fis_measure/interfaces/date_types/point.dart';
-import 'package:fis_measure/interfaces/date_types/vector.dart';
 import 'package:fis_measure/interfaces/enums/items.dart';
 import 'package:fis_measure/interfaces/process/items/item.dart';
 import 'package:fis_measure/interfaces/process/items/item_metas.dart';
-import 'package:fis_measure/interfaces/process/workspace/application.dart';
 import 'package:fis_measure/interfaces/process/workspace/point_info.dart';
 import 'package:fis_measure/process/calcuators/semiauto_trace.dart';
 import 'package:fis_measure/process/items/item.dart';
@@ -29,7 +27,6 @@ class SemiautoTrace extends TraceItemAbstract {
         state = ItemStates.waiting;
       }
     }
-    final point = args.toAreaLogicPoint();
 
     if (state == ItemStates.waiting) {
       if (args.pointType == PointInfoType.mouseDown) {
@@ -42,15 +39,13 @@ class SemiautoTrace extends TraceItemAbstract {
       if (args.pointType == PointInfoType.mouseDown) {
         List<CardiacCycle> currentCardiacCycleList =
             feature?.refItem.currentCardiacCycleList ?? [];
+        print(currentCardiacCycleList.length);
         if (currentCardiacCycleList.isNotEmpty) {
           feature?.refItem.setInnerPoints([
-            convertToAreaPoint(currentCardiacCycleList.first.systoleStart)
-                .addVector(DVector(convertToAreaPoint(DPoint(-1, 0)).x,
-                    convertToAreaPoint(DPoint(-1, 0)).y)),
-            convertToAreaPoint(currentCardiacCycleList.last.diastoleEnd)
+            currentCardiacCycleList.first.systoleStart,
+            currentCardiacCycleList.last.diastoleEnd
           ]);
         }
-
         feature?.refItem.setCurrentCardiacCycleList([]);
         doFeatureFinish();
       }
@@ -58,15 +53,15 @@ class SemiautoTrace extends TraceItemAbstract {
     return true;
   }
 
-  DPoint convertToAreaPoint(DPoint point) {
-    /// 像素坐标
-    final x = point.x;
-    final y = point.y;
-    final application = Get.find<IApplication>();
-    Size displaySize = application.displaySize;
+  // DPoint convertToAreaPoint(DPoint point) {
+  //   /// 像素坐标
+  //   final x = point.x;
+  //   final y = point.y;
+  //   final application = Get.find<IApplication>();
+  //   Size displaySize = application.displaySize;
 
-    return DPoint(x / displaySize.width, y / displaySize.height);
-  }
+  //   return DPoint(x / displaySize.width, y / displaySize.height);
+  // }
 
   @override
   void doFeatureFinish() {
@@ -80,6 +75,8 @@ class SemiautoTrace extends TraceItemAbstract {
   }
 
   void handleMouseDownWhileWaiting(PointInfo args) {
+    print("正在画");
+
     // TODO: 判断是否当前area
     // 转换为Area逻辑位置
     feature = SemiautoTraceFeature(this);
@@ -92,6 +89,7 @@ class SemiautoTrace extends TraceItemAbstract {
   }
 
   void handleTouchDownWhileWaiting(PointInfo args) {
+    print("画结束了");
     // TODO: 判断是否当前area
     // 转换为Area逻辑位置
     feature = SemiautoTraceFeature(this);
@@ -185,9 +183,11 @@ class SemiautoTraceFeature extends SemiautoTraceItemFeatureAbstract {
     ..isAntiAlias = false
     ..strokeWidth = 1
     ..style = PaintingStyle.stroke;
+
   DPoint furthestPoint = DPoint(0, 0);
-  static const double threshold = 1;
+
   List<CardiacCycle> currentCardiacCycleList = [];
+  late Path path;
 
   @override
   void paint(Canvas canvas, Size size) {
@@ -200,9 +200,7 @@ class SemiautoTraceFeature extends SemiautoTraceItemFeatureAbstract {
       drawId(canvas, size);
       return;
     }
-    double maxDistance = 0;
     drawId(canvas, size);
-    Path path = Path();
 
     final points = innerPoints.map((e) => convert2ViewPoint(size, e)).toList();
     final startOffset = convert2ViewPoint(size, startPoint);
@@ -215,40 +213,75 @@ class SemiautoTraceFeature extends SemiautoTraceItemFeatureAbstract {
 
     double baseLineHeight = areaTop + baseLine * pwHeight;
 
-    var data = TraceListData.instance;
+    // TraceListData data = TraceListData();
+    List<DPoint> maxPonints = [];
+    List<DPoint> logicalPoints = [];
+
+    /// 当前周期数据
+    List<CardiacCycle> cardiacCycleList = [];
+    List<CardiacCycle> logicalCardiacCycleList = [];
+    path = Path();
 
     /// 最大点集
-    List<DPoint> maxPonints = data.maxPonints;
+    /// below: 1
+    /// above: 2
     if (startOffset.y < baseLineHeight) {
-      data.setLocation(TraceLocationType.above);
+      // data.setLocation("above");
+      logicalPoints = List.generate(
+        TraceListData.aboveMaxPonints.length,
+        (index) => TraceListData.aboveMaxPonints[index],
+      );
+      logicalCardiacCycleList = List.generate(
+        TraceListData.aboveCardiacCycleList.length,
+        (index) => TraceListData.aboveCardiacCycleList[index],
+      );
     } else {
-      data.setLocation(TraceLocationType.below);
+      logicalPoints = List.generate(
+        TraceListData.belowMaxPonints.length,
+        (index) => TraceListData.belowMaxPonints[index],
+      );
+      logicalCardiacCycleList = List.generate(
+        TraceListData.belowCardiacCycleList.length,
+        (index) => TraceListData.belowCardiacCycleList[index],
+      );
     }
 
-    /// 当前周期数据
-    List<CardiacCycle> cardiacCycleList = data.cardiacCycleList;
+    maxPonints = logicalPoints.map((e) => convert2ViewPoint(size, e)).toList();
+    for (int i = 0; i < logicalCardiacCycleList.length; i++) {
+      logicalCardiacCycleList[i].index = i;
+      cardiacCycleList.add(logicalCardiacCycleList[i]);
+    }
 
+    /// 这是正在绘制的
     if (points.length > 1) {
-      DPoint startDPoint = maxPonints.firstWhere(
-        (element) => element.x.toInt() == startOffset.x.toInt(),
-      );
-      path.moveTo(startDPoint.x, startDPoint.y);
-
       final firstPoint = points.first;
       final lastPoint = points.last;
+      DPoint? startDPoint = maxPonints.firstWhereOrNull(
+        (element) => element.x.toInt() == startOffset.x.toInt(),
+      );
       var firstIndex = maxPonints.indexWhere(
         (element) => element.x.toInt() == firstPoint.x.toInt(),
       );
       var lastIndex = maxPonints.indexWhere(
         (element) => element.x.toInt() == lastPoint.x.toInt(),
       );
-      List<DPoint> points2 = [];
+
+      if (firstIndex == -1) {
+        firstIndex = 0;
+      }
+      if (startPoint == null) {
+        return;
+      }
+
       path = Path();
+      path.moveTo(startDPoint!.x, startDPoint.y);
+      List<DPoint> points2 = [];
       path.moveTo(maxPonints[firstIndex].x, maxPonints[firstIndex].y);
       for (int i = 0; i < maxPonints.length; i++) {
-        if (firstIndex == lastIndex) {
-          return;
+        if (lastIndex == -1) {
+          lastIndex = maxPonints.length - 1;
         }
+
         if (firstIndex > lastIndex) {
           points2 = maxPonints.sublist(lastIndex, firstIndex);
         } else {
@@ -259,142 +292,74 @@ class SemiautoTraceFeature extends SemiautoTraceItemFeatureAbstract {
       if (firstIndex > lastIndex) {
         for (var i = points2.length - 1; i > 1; i--) {
           final point = points2[i];
-          DPoint endDPoint = maxPonints
-              .firstWhere((element) => element.x.toInt() == point.x.toInt());
+          DPoint endDPoint = maxPonints.firstWhere(
+            (element) => element.x.toInt() == point.x.toInt(),
+          );
           path.lineTo(endDPoint.x, endDPoint.y);
-          // final distance = (endDPoint.y - startOffset.y).abs();
-          // if (distance > maxDistance) {
-          //   maxDistance = distance;
-          //   furthestPoint = endDPoint;
-          // }
         }
       } else {
         for (var i = 1; i < points2.length; i++) {
           final point = points2[i];
-          DPoint endDPoint = maxPonints
-              .firstWhere((element) => element.x.toInt() == point.x.toInt());
+          DPoint endDPoint = maxPonints.firstWhere(
+            (element) => element.x.toInt() == point.x.toInt(),
+          );
           path.lineTo(endDPoint.x, endDPoint.y);
-          // final distance = (endDPoint.y - startOffset.y).abs();
-          // if (distance > maxDistance) {
-          //   maxDistance = distance;
-          //   furthestPoint = endDPoint;
-          // }
         }
       }
 
       /// 根据周期绘制
       for (CardiacCycle i in cardiacCycleList) {
-        if (i.diastoleEnd.x <= endOffset.x &&
-            i.systoleStart.x >= startOffset.x) {
-          canvas.drawDashLine(
-            Offset(i.systoleStart.x, areaTop),
-            Offset(i.systoleStart.x, areaBottom),
-            3,
-            3,
-            dashLinePan,
-          );
-          canvas.drawDashLine(
-            Offset(i.diastoleEnd.x, areaTop),
-            Offset(i.diastoleEnd.x, areaBottom),
-            3,
-            3,
-            dashLinePan,
-          );
-          drawCrossVertex(
-            canvas,
-            TraceListData.convert(i.peakSystolic).toOffset(),
-          );
-          if (!currentCardiacCycleList.contains(i)) {
-            currentCardiacCycleList.add(i);
-          }
-        } else if (i.diastoleEnd.x > endOffset.x) {
-          if (currentCardiacCycleList.contains(i)) {
-            currentCardiacCycleList.remove(i);
+        CardiacCycle? currentCycle = currentCardiacCycleList.firstWhereOrNull(
+          (element) => element.index == i.index,
+        );
+        DPoint diastoleEnd = convert2ViewPoint(size, i.diastoleEnd);
+        DPoint systoleStart = convert2ViewPoint(size, i.systoleStart);
+        DPoint ePeak = convert2ViewPoint(size, i.ePeak ?? DPoint(0, 0));
+
+        /// ai提供的周期数据可能不对
+        if (systoleStart.x <= diastoleEnd.x) {
+          if (startOffset.x <= systoleStart.x && endOffset.x >= diastoleEnd.x) {
+            if (systoleStart.x >= startOffset.x) {
+              canvas.drawDashLine(
+                Offset(systoleStart.x, areaTop),
+                Offset(systoleStart.x, areaBottom),
+                3,
+                3,
+                dashLinePan,
+              );
+              canvas.drawDashLine(
+                Offset(diastoleEnd.x, areaTop),
+                Offset(diastoleEnd.x, areaBottom),
+                3,
+                3,
+                dashLinePan,
+              );
+              if (ePeak.x < endOffset.x) {
+                drawCrossVertex(
+                  canvas,
+                  ePeak.toOffset(),
+                );
+              }
+            }
+            if (currentCycle == null) {
+              currentCardiacCycleList.add(i);
+            }
+          } else {
+            if (currentCycle != null) {
+              currentCardiacCycleList.remove(i);
+            }
           }
         }
       }
       refItem
           .setCurrentCardiacCycleList(currentCardiacCycleList.toSet().toList());
 
-      // // drawCrossVertex(canvas, furthestPoint.toOffset());
-      // for (SemiautoTraceItemFeatureAbstract i in refItem.measuredFeatures) {
-      //   path = Path();
-      //   CardiacCycle firstCardiacCycle =
-      //       i.refItem.currentCardiacCycleList.first;
-      //   CardiacCycle lastCardiacCycle = i.refItem.currentCardiacCycleList.last;
-      //   path.moveTo(
-      //       firstCardiacCycle.systoleStart.x, firstCardiacCycle.systoleStart.y);
-      //   //     for(CardiacCycle i in currentCardiacCycleList) {
-      //   //   path.lineTo(i.peakSystolic.x, i.peakSystolic.y);
-      //   // }
-      //   var firstIndex = maxPonints.indexWhere(
-      //     (element) =>
-      //         element.x.toInt() == firstCardiacCycle.systoleStart.x.toInt(),
-      //   );
-      //   var lastIndex = maxPonints.indexWhere(
-      //     (element) =>
-      //         element.x.toInt() == lastCardiacCycle.diastoleEnd.x.toInt(),
-      //   );
-      //   List<DPoint> points = [];
-      //   for (int i = 0; i < maxPonints.length; i++) {
-      //     points = maxPonints.sublist(firstIndex, lastIndex);
-      //   }
-      //   print(points);
-      //   for (DPoint i in points) {
-      //     path.lineTo(i.x, i.y);
-      //   }
-      // }
-
-      // if (refItem.feature != null &&
-      //     refItem.feature!.id < refItem.measuredFeatures.length &&
-      //     currentCardiacCycleList.isNotEmpty) {
-      //   path = Path();
-      //   CardiacCycle firstCardiacCycle = currentCardiacCycleList.first;
-      //   CardiacCycle lastCardiacCycle = currentCardiacCycleList.last;
-      //   path.moveTo(
-      //       firstCardiacCycle.systoleStart.x, firstCardiacCycle.systoleStart.y);
-      //   //     for(CardiacCycle i in currentCardiacCycleList) {
-      //   //   path.lineTo(i.peakSystolic.x, i.peakSystolic.y);
-      //   // }
-      //   var firstIndex = maxPonints.indexWhere(
-      //     (element) =>
-      //         element.x.toInt() == firstCardiacCycle.systoleStart.x.toInt(),
-      //   );
-      //   var lastIndex = maxPonints.indexWhere(
-      //     (element) =>
-      //         element.x.toInt() == lastCardiacCycle.diastoleEnd.x.toInt(),
-      //   );
-      //   List<DPoint> points = [];
-      //   for (int i = 0; i < maxPonints.length; i++) {
-      //     points = maxPonints.sublist(firstIndex, lastIndex);
-      //   }
-      //   print(points);
-      //   for (DPoint i in points) {
-      //     path.lineTo(i.x, i.y);
-      //   }
-      //   //     for(CardiacCycle i in currentCardiacCycleList) {
-      //   //   path.lineTo(i.peakSystolic.x, i.peakSystolic.y);
-      //   // }
-      // }
       canvas.drawPath(
         path,
         greenPen,
       );
     }
   }
-
-  /// 转成物理坐标
-  DPoint convert2LogicPoint(
-    Size size,
-    DPoint viewPoint,
-    double baseLine,
-    double width,
-    double height,
-  ) {
-    final x = viewPoint.x / size.width * width;
-    final y = viewPoint.y - (baseLine * size.height) * height;
-    return DPoint(x, y);
-  }
 }
 
 abstract class SemiautoTraceItemFeatureAbstract extends MeasureItemFeature {
@@ -407,6 +372,7 @@ abstract class SemiautoTraceItemFeatureAbstract extends MeasureItemFeature {
   DPoint get endPoint => innerPoints.last;
 
   bool ifRightSide = true;
+  // String currentLocal = "11111";
 
   /// 接收新坐标
   void adopt(DPoint point) {
@@ -478,4 +444,8 @@ abstract class TraceItemAbstract
     feature!.innerPoints.clear();
     feature!.innerPoints.addAll(points);
   }
+
+  void clearInnerPoints() {
+    feature!.innerPoints.clear();
+  }
 }

+ 5 - 7
lib/view/player/controller.dart

@@ -11,6 +11,7 @@ import 'package:fis_jsonrpc/rpc.dart';
 import 'package:fis_measure/interfaces/date_types/rect_region.dart';
 import 'package:fis_measure/interfaces/process/player/play_controller.dart';
 import 'package:fis_measure/interfaces/process/workspace/application.dart';
+import 'package:fis_measure/process/primitives/multi_method/dop_trace_disp/data.dart';
 import 'package:fis_measure/process/workspace/rpc_bridge.dart';
 import 'package:fis_measure/view/player/buffer_waiter.dart';
 import 'package:fis_vid/data_channel/channel.dart';
@@ -241,8 +242,8 @@ class VidPlayerController extends ChangeNotifier implements IPlayerController {
       _bufferWaiter.recordFrameSpendTime(spendTime);
     }
 
-    // ///[TODO] 半自动 半自动自动测量获取的ai数据
-    // _getHalfAutoAiResult();
+    ///[TODO] 半自动 半自动自动测量获取的ai数据
+    _getHalfAutoAiResult();
     return result;
   }
 
@@ -371,7 +372,7 @@ class VidPlayerController extends ChangeNotifier implements IPlayerController {
       return false;
     });
     if (currentFrame != null && rectRegion != null) {
-      var a = await getAutoTraceImageData(
+      VetAutoTraceImageResult? autoTraceImageData = await getAutoTraceImageData(
         imageInfo: VetAutoTraceImageDTO(
           imageBase64String: base64Encode(currentFrame!.imageData),
           height: currentFrame!.height,
@@ -387,10 +388,7 @@ class VidPlayerController extends ChangeNotifier implements IPlayerController {
           width: ((rectRegion?.width ?? 0) * currentFrame!.width).toInt(),
         ),
       );
-
-      print("🍭🍭🍭🍭🍭");
-      print(a);
-      print("🍭🍭🍭🍭🍭");
+      TraceListData.setVetAutoTraceImageResult(autoTraceImageData);
     }
   }
 

+ 1 - 1
pubspec.yaml

@@ -100,7 +100,7 @@ dependency_overrides:
   fis_jsonrpc:
     git:
       url: http://git.ius.plus:88/Project-Wing/fis_lib_jsonrpc.git
-      ref: "cd3bcf1"
+      ref: "9671c5a"
   fis_lib_business_components:
     git:
       url: http://git.ius.plus/Project-Wing/fis_lib_business_components.git