Browse Source

移动端支持切换测量区域,新增测量区域指示器

gavin.chen 2 years ago
parent
commit
d6a784445d

+ 0 - 1
lib/interfaces/process/visuals/visual_area.dart

@@ -1,6 +1,5 @@
 import 'package:fis_common/event/event_type.dart';
 import 'package:fis_measure/interfaces/date_types/rect_region.dart';
-import 'package:fis_measure/interfaces/enums/visual_area_type.dart';
 import 'package:fis_measure/interfaces/process/visuals/visual.dart';
 import 'package:flutter/foundation.dart';
 import 'package:vid/us/vid_us_visual_area_type.dart';

+ 8 - 6
lib/view/canvas/records_canvas.dart

@@ -39,12 +39,14 @@ class _RecordsCanvasPanelState extends State<MeasureRecordsCanvasPanel> {
     }
 
     return LayoutBuilder(builder: (context, constraints) {
-      return SizedBox(
-        width: constraints.maxWidth,
-        height: constraints.maxHeight,
-        child: RepaintBoundary(
-          child: CustomPaint(
-            painter: _PanelPainter(measureFeatures, annotationFeatures),
+      return ClipRect(
+        child: SizedBox(
+          width: constraints.maxWidth,
+          height: constraints.maxHeight,
+          child: RepaintBoundary(
+            child: CustomPaint(
+              painter: _PanelPainter(measureFeatures, annotationFeatures),
+            ),
           ),
         ),
       );

+ 9 - 0
lib/view/mobile_view/mobile_measure_main_view.dart

@@ -25,6 +25,7 @@ import 'package:fis_measure/view/mobile_view/mobile_bottom_menu.dart';
 import 'package:fis_measure/view/mobile_view/mobile_right_panel.dart';
 import 'package:fis_measure/view/mobile_view/mobile_top_menu.dart';
 import 'package:fis_measure/view/mobile_view/widgets/magnifier.dart';
+import 'package:fis_measure/view/outline/measure_area_indicator.dart';
 import 'package:fis_measure/view/paint/ai_patint.dart';
 import 'package:fis_measure/view/paint/ai_patint_controller.dart';
 import 'package:fis_measure/view/result/results_panel.dart';
@@ -410,6 +411,10 @@ class _MobileMeasureMainViewState extends State<MobileMeasureMainView> {
                       child: const SizedBox(
                           height: 200, child: MeasureResultPanel()),
                     ),
+                    LayoutId(
+                      id: _LayerLayoutIds.currActiveAreaIndicator,
+                      child: const MeasureAreaIndicator(),
+                    ),
                     LayoutId(
                       id: _LayerLayoutIds.canvasMagnifier,
                       child: const CanvasMagnifier(),
@@ -478,6 +483,7 @@ class _LayerLayoutDelegate extends MultiChildLayoutDelegate {
     layoutLayer(_LayerLayoutIds.activeMeasure, offset, renderSize);
     layoutLayer(_LayerLayoutIds.activeAnnotation, offset, renderSize);
     layoutLayer(_LayerLayoutIds.standardLineCalibration, offset, renderSize);
+    layoutLayer(_LayerLayoutIds.currActiveAreaIndicator, offset, renderSize);
     layoutLayer(_LayerLayoutIds.canvasMagnifier, offset, renderSize);
     layoutLayer(_LayerLayoutIds.gesture, offset, renderSize);
     layoutLayer(
@@ -561,4 +567,7 @@ enum _LayerLayoutIds {
 
   /// 参考校准线画板
   standardLineCalibration,
+
+  /// 当前测量区域指示器
+  currActiveAreaIndicator,
 }

+ 98 - 6
lib/view/mobile_view/mobile_right_panel/mobile_measure_tool.dart

@@ -6,16 +6,19 @@ import 'package:fis_measure/interfaces/process/items/item_metas.dart';
 import 'package:fis_measure/interfaces/process/items/terms.dart';
 import 'package:fis_measure/interfaces/process/items/types.dart';
 import 'package:fis_measure/interfaces/process/player/play_controller.dart';
+import 'package:fis_measure/interfaces/process/visuals/visual_area.dart';
 import 'package:fis_measure/interfaces/process/workspace/application.dart';
 import 'package:fis_measure/process/language/measure_language.dart';
 import 'package:fis_measure/process/workspace/measure_data_controller.dart';
 import 'package:fis_measure/process/workspace/measure_handler.dart';
+import 'package:fis_measure/utils/prompt_box.dart';
 import 'package:fis_measure/view/mobile_view/mobile_right_panel/mobile_more_measure_item_dialog.dart';
 import 'package:fis_ui/index.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:get/get.dart';
 import 'package:vid/us/vid_us_unit.dart';
+import 'package:vid/us/vid_us_visual_area_type.dart';
 
 /// 移动端测量项选择器(继承自 测量项页面)
 class MobileMeasureSelector extends FStatefulWidget {
@@ -32,6 +35,9 @@ class _MobileMeasureSelector extends FState<MobileMeasureSelector> {
   late final measureData = Get.find<MeasureDataController>();
   late final playerController = Get.find<IPlayerController>();
 
+  /// 测量语言包
+  final measureLanguage = MeasureLanguage();
+
   /// 当前选中的测量项目
   String activeName = "";
 
@@ -93,12 +99,25 @@ class _MobileMeasureSelector extends FState<MobileMeasureSelector> {
     ),
   ];
 
+  /// 支持基础测量项的区域类型
+  final List<VidUsVisualAreaType> _supportBasicMeasureAreaTypes = [
+    VidUsVisualAreaType.Tissue,
+    VidUsVisualAreaType.Flow,
+  ];
+
+  /// 是否允许使用基础测量项
+  bool get _isEnableBaseMeasureItems =>
+      _supportBasicMeasureAreaTypes.contains(_currentAreaType);
+
   /// 是否已初始化完成所有测量项数据
   bool _isInitAllMeasureItems = false;
 
   /// 可用的模式 (测量项包含在内)
   List<MobileMoreMeasureItemModesModel> _availableModes = [];
 
+  /// 当前的区域类型
+  VidUsVisualAreaType? _currentAreaType = VidUsVisualAreaType.Tissue;
+
   /// 切换测量项
   void changeItem(ItemMeta itemMeta) {
     if (mounted) {
@@ -145,9 +164,23 @@ class _MobileMeasureSelector extends FState<MobileMeasureSelector> {
       if (mounted) {
         setState(() {});
       }
+    } else {
+      /// 如果为空测量项
+      isCombo = false;
+      activeChildItemIndex = 0;
+      showMore = false;
+      activeName = "";
+      if (mounted) {
+        setState(() {});
+      }
     }
   }
 
+  void _visualAreaChanged(sender, IVisualArea e) {
+    _cancelCurrSelect();
+    _currentAreaType = e.visualAreaType;
+  }
+
   @override
   FWidget build(BuildContext context) {
     return FSizedBox(
@@ -163,6 +196,8 @@ class _MobileMeasureSelector extends FState<MobileMeasureSelector> {
     super.initState();
     application.activeMeasureItemChanged
         .addListener(_onActiveMeasureItemChanged);
+    application.visualAreaChanged.addListener(_visualAreaChanged);
+    _currentAreaType = application.currentVisualArea.visualAreaType;
   }
 
   @override
@@ -170,6 +205,7 @@ class _MobileMeasureSelector extends FState<MobileMeasureSelector> {
     super.dispose();
     application.activeMeasureItemChanged
         .removeListener(_onActiveMeasureItemChanged);
+    application.visualAreaChanged.removeListener(_visualAreaChanged);
     final item = application.activeMeasureItem;
     if (item != null && item is ITopMeasureItem) {
       item.workingChildChanged.removeListener(_onWorkingChildChanged);
@@ -249,7 +285,12 @@ class _MobileMeasureSelector extends FState<MobileMeasureSelector> {
         if (ifActive) {
           _cancelCurrSelect();
         } else {
-          changeItem(itemMeta);
+          if (_isEnableBaseMeasureItems) {
+            changeItem(itemMeta);
+          } else {
+            /// TODO:[Gavin] i18n
+            PromptBox.toast("当前区域不支持该测量项");
+          }
         }
       },
       child: Container(
@@ -401,9 +442,16 @@ class _MobileMeasureSelector extends FState<MobileMeasureSelector> {
       modes = await _initMeasureItemsList();
       _availableModes = modes;
     }
+    final List<String> _currModes =
+        _findModesByCurrArea(application.currentVisualArea);
+
+    final List<MobileMoreMeasureItemModesModel> activeModes = modes
+        .where((element) => _currModes.contains(element.modeName))
+        .toList();
+
     final ItemMeta? result =
         await Get.dialog<ItemMeta>(MobileMoreMeasureItemDialog(
-      measureModeList: modes,
+      measureModeList: activeModes,
       activeItemName: activeName,
     ));
     if (result != null) {
@@ -414,6 +462,21 @@ class _MobileMeasureSelector extends FState<MobileMeasureSelector> {
     }
   }
 
+  /// 可能有些区域会重叠,无法手动区分,所以需要根据当前的区域来查找可以使用的测量项
+  List<String> _findModesByCurrArea(IVisualArea currArea) {
+    List<String> modes = [];
+    List<IVisualArea> visualAreas = application.visuals[0].visualAreas;
+    if (visualAreas.isEmpty) {
+      return modes;
+    }
+    for (IVisualArea element in visualAreas) {
+      if (element.displayRegion == currArea.displayRegion) {
+        modes.add(element.mode.modeType.toString().split('.')[1]);
+      }
+    }
+    return modes;
+  }
+
   /// 取消当前选中的测量项
   void _cancelCurrSelect() {
     changeItem(
@@ -457,10 +520,13 @@ class _MobileMeasureSelector extends FState<MobileMeasureSelector> {
     for (int i = 0; i < measureApplicationDTO.availableModes!.length; i++) {
       MeasureModeDTO mode = measureApplicationDTO.availableModes![i];
       List<ItemMetaDTO> supportedItems = _currSupportedItemFilter(mode);
-      String modeName = displayModeNames.length > i ? displayModeNames[i] : '';
+      String displayName =
+          displayModeNames.length > i ? displayModeNames[i] : '';
       MobileMoreMeasureItemModesModel modeModel =
           MobileMoreMeasureItemModesModel(
-              modeName: modeName, availableItems: supportedItems);
+              modeName: mode.modeName ?? '',
+              displayName: displayName,
+              availableItems: _itemConverter(supportedItems));
       supportedModes.add(modeModel);
     }
     if (supportedModes.isNotEmpty) {
@@ -469,6 +535,29 @@ class _MobileMeasureSelector extends FState<MobileMeasureSelector> {
     return supportedModes;
   }
 
+  List<MeasureItemModel> _itemConverter(List<ItemMetaDTO> items) {
+    if (i18nBook.isCurrentChinese) {
+      return items.map((e) {
+        final cName = measureLanguage.t('measure', e.description ?? '');
+        return MeasureItemModel(
+          displayName: cName,
+          itemMeta: e,
+          category: e.categories?.first ?? '',
+          searchKey: "${e.name?.toLowerCase()}$cName",
+        );
+      }).toList();
+    } else {
+      return items.map((e) {
+        return MeasureItemModel(
+          displayName: e.name ?? '',
+          itemMeta: e,
+          category: e.categories?.first ?? '',
+          searchKey: "${e.name?.toLowerCase()}",
+        );
+      }).toList();
+    }
+  }
+
   /// 过滤出当前支持的所有测量项
   List<ItemMetaDTO> _currSupportedItemFilter(MeasureModeDTO mode) {
     final supportedMeasureTypeName = ['Distance', 'AreaPerimeterTrace'];
@@ -530,7 +619,10 @@ class MobileBaseMeasureItemBtn {
 
 class MobileMoreMeasureItemModesModel {
   String modeName;
-  List<ItemMetaDTO> availableItems;
+  String displayName;
+  List<MeasureItemModel> availableItems;
   MobileMoreMeasureItemModesModel(
-      {required this.modeName, required this.availableItems});
+      {required this.modeName,
+      required this.displayName,
+      required this.availableItems});
 }

+ 132 - 32
lib/view/mobile_view/mobile_right_panel/mobile_more_measure_item_dialog.dart

@@ -3,7 +3,6 @@ import 'package:fis_i18n/i18n.dart';
 import 'package:fis_jsonrpc/services/remedical.m.dart';
 import 'package:fis_measure/interfaces/process/items/item_metas.dart';
 import 'package:fis_measure/process/items/item_meta_convert.dart';
-import 'package:fis_measure/process/language/measure_language.dart';
 import 'package:fis_measure/view/mobile_view/mobile_right_panel/mobile_measure_tool.dart';
 import 'package:fis_ui/index.dart';
 import 'package:flutter/material.dart';
@@ -26,37 +25,34 @@ class _MobileMoreMeasureItemDialogState
   final scrollController = ScrollController();
   final searchBarController = TextEditingController();
 
-  /// 测量语言包
-  final measureLanguage = MeasureLanguage();
-
   /// 当前模式的下标
   int currentModeIndex = 0;
 
-  /// 当前模式下可用的测量项
-  List<ItemMetaDTO> currentModeMeasureItemList = [];
-
   /// 当前可用的模式列表
   List<String> currentModeNameList = [];
 
   /// 搜索词
   String searchWord = '';
 
-  /// 搜索结果
-  List<ItemMetaDTO> searchResult = [];
+  /// 当前模式下可用的测量项
+  List<MeasureItemModel> currentModeMeasureItemList = [];
+
+  /// 过滤后的展示结果
+  List<MeasureItemModel> filteredItems = [];
 
   @override
   void initState() {
     super.initState();
     // 遍历 measureModeList 获取到当前可用的模式列表,写入 currentModeNameList
     for (final measureMode in widget.measureModeList) {
-      currentModeNameList.add(measureMode.modeName);
+      currentModeNameList.add(measureMode.displayName);
     }
     if (widget.measureModeList.isEmpty) {
       return;
     }
     currentModeMeasureItemList =
         widget.measureModeList[currentModeIndex].availableItems;
-    searchResult = currentModeMeasureItemList.toList();
+    filteredItems = currentModeMeasureItemList.toList();
   }
 
   @override
@@ -141,7 +137,7 @@ class _MobileMoreMeasureItemDialogState
         ),
       );
     }
-    if (searchResult.isEmpty) {
+    if (filteredItems.isEmpty) {
       return FContainer(
         color: const Color.fromARGB(255, 36, 36, 36),
         child: FCenter(
@@ -155,50 +151,89 @@ class _MobileMoreMeasureItemDialogState
         ),
       );
     }
+
+    final List<MeasureItemCategoryModel> categories =
+        _getClassifiedData(filteredItems);
     return FContainer(
       color: const Color.fromARGB(255, 36, 36, 36),
       child: FScrollbar(
         controller: scrollController,
         isAlwaysShown: true,
-        child: FGridView.count(
-          padding: const EdgeInsets.all(10),
+        child: FListView(
+          shrinkWrap: true,
           controller: scrollController,
+          children: [
+            FColumn(
+              children: [
+                ...categories.map((e) {
+                  return _buildEachCategory(e);
+                }).toList(),
+              ],
+            )
+          ],
+        ),
+      ),
+    );
+  }
+
+  FWidget _buildEachCategory(MeasureItemCategoryModel category) {
+    final List<MeasureItemModel> measureItemList = category.items;
+    final String categoryName = category.name;
+    return FColumn(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        FContainer(
+          margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 2),
+          child: FText(
+            categoryName,
+            style: const TextStyle(
+              color: Colors.grey,
+              fontSize: 14,
+            ),
+            textAlign: TextAlign.start,
+          ),
+        ),
+        FContainer(
+          margin: const EdgeInsets.symmetric(horizontal: 10),
+          color: Colors.grey,
+          height: 1,
+        ),
+        FGridView.count(
+          physics: const NeverScrollableScrollPhysics(),
+          shrinkWrap: true,
+          padding: const EdgeInsets.all(10),
           crossAxisCount: 6,
           childAspectRatio: 3,
           mainAxisSpacing: 5,
           crossAxisSpacing: 5,
           children: [
-            ...searchResult.map((e) {
+            ...measureItemList.map((e) {
               return _buildMeasureListItem(
                 e,
               );
             }).toList()
           ],
         ),
-      ),
+      ],
     );
   }
 
   /// 构建更多测量项列表中的测量项按钮
   FWidget _buildMeasureListItem(
-    ItemMetaDTO itemMeta,
+    MeasureItemModel item,
   ) {
-    final bool isActive = widget.activeItemName == itemMeta.name;
-    String displayName = itemMeta.name ?? '';
-    if (i18nBook.isCurrentChinese) {
-      displayName = measureLanguage.t('measure', itemMeta.description ?? '');
-    }
+    final bool isActive = widget.activeItemName == item.itemMeta.name;
     return FInkWell(
       onTap: () {
         if (isActive) return;
         HapticFeedback.mediumImpact();
         ItemMeta selectedItemMeta;
         try {
-          selectedItemMeta = ItemMetaConverter(itemMeta).output();
+          selectedItemMeta = ItemMetaConverter(item.itemMeta).output();
           Get.back<ItemMeta>(result: selectedItemMeta);
         } catch (e) {
           logger.e(
-            "Item meta -[${itemMeta.name}] convert error; JSON-Text: ${itemMeta.toJson()}",
+            "Item meta -[${item.displayName}] convert error; JSON-Text: ${item.itemMeta.toJson()}",
             e,
           );
         }
@@ -213,7 +248,7 @@ class _MobileMoreMeasureItemDialogState
         ),
         child: FCenter(
           child: FText(
-            displayName,
+            item.displayName,
             textAlign: TextAlign.center,
             style: TextStyle(
               fontSize: 12,
@@ -300,7 +335,7 @@ class _MobileMoreMeasureItemDialogState
                 border: InputBorder.none,
               ),
               onChanged: (value) {
-                _searchMeasureItem(value);
+                _filterMeasureItems(value);
               },
             ),
           ),
@@ -309,13 +344,13 @@ class _MobileMoreMeasureItemDialogState
     );
   }
 
-  void _searchMeasureItem(String value) {
+  void _filterMeasureItems(String value) {
     searchWord = value;
     value = value.toLowerCase();
-    searchResult.clear(); // 清除任何现有的搜索结果
-    for (ItemMetaDTO itemMeta in currentModeMeasureItemList) {
-      if (itemMeta.name!.toLowerCase().contains(value)) {
-        searchResult.add(itemMeta);
+    filteredItems.clear(); // 清除任何现有的搜索结果
+    for (MeasureItemModel itemModel in currentModeMeasureItemList) {
+      if (itemModel.searchKey.contains(value)) {
+        filteredItems.add(itemModel);
       }
     }
     setState(() {});
@@ -325,6 +360,71 @@ class _MobileMoreMeasureItemDialogState
     currentModeIndex = index;
     currentModeMeasureItemList =
         widget.measureModeList[currentModeIndex].availableItems;
-    _searchMeasureItem(searchWord);
+    _filterMeasureItems(searchWord);
+  }
+
+  List<MeasureItemCategoryModel> _getClassifiedData(
+    List<MeasureItemModel> items,
+  ) {
+    final Map<String, List<MeasureItemModel>> categoryMap = {};
+    for (MeasureItemModel item in items) {
+      if (categoryMap.containsKey(item.category)) {
+        categoryMap[item.category]!.add(item);
+      } else {
+        categoryMap[item.category] = [item];
+      }
+    }
+    List<MeasureItemCategoryModel> classifiedData = categoryMap.entries
+        .map((e) => MeasureItemCategoryModel(
+              name: e.key,
+              items: e.value,
+            ))
+        .toList();
+
+    /// 遍历 classifiedData 将 common 类别放在最前面
+    int commonIndex = classifiedData
+        .indexWhere((element) => element.name.toLowerCase().contains('common'));
+    if (commonIndex != -1) {
+      final MeasureItemCategoryModel commonCategory =
+          classifiedData[commonIndex];
+      classifiedData.removeAt(commonIndex);
+      classifiedData.insert(0, commonCategory);
+    }
+
+    return classifiedData;
   }
 }
+
+class MeasureItemModel {
+  /// 外显名称(中文下只显示中文、英文下只显示英文)
+  final String displayName;
+
+  /// 测量项DTO
+  final ItemMetaDTO itemMeta;
+
+  /// 类别
+  final String category;
+
+  /// 搜索关键字
+  final String searchKey;
+
+  MeasureItemModel({
+    required this.displayName,
+    required this.itemMeta,
+    required this.category,
+    required this.searchKey,
+  });
+}
+
+class MeasureItemCategoryModel {
+  /// 类别名称
+  final String name;
+
+  /// 类别下的测量项
+  final List<MeasureItemModel> items;
+
+  MeasureItemCategoryModel({
+    required this.name,
+    required this.items,
+  });
+}

+ 94 - 0
lib/view/outline/measure_area_indicator.dart

@@ -0,0 +1,94 @@
+import 'package:fis_measure/interfaces/process/visuals/visual_area.dart';
+import 'package:fis_measure/interfaces/process/workspace/application.dart';
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:vid/us/vid_us_visual_area_type.dart';
+
+class MeasureAreaIndicator extends StatefulWidget {
+  const MeasureAreaIndicator({super.key});
+
+  @override
+  State<MeasureAreaIndicator> createState() => _MeasureAreaIndicatorState();
+}
+
+class _MeasureAreaIndicatorState extends State<MeasureAreaIndicator> {
+  late IApplication application = Get.find<IApplication>();
+
+  /// 高度百分比
+  double regionHeight = 0;
+
+  /// 宽度百分比
+  double regionWidth = 0;
+
+  /// Top百分比
+  double regionTop = 0;
+
+  /// Left百分比
+  double regionLeft = 0;
+
+  @override
+  void initState() {
+    super.initState();
+    application.visualAreaChanged.addListener(_visualAreaChanged);
+    _initVisualArea(application.currentVisualArea);
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    application.visualAreaChanged.removeListener(_visualAreaChanged);
+  }
+
+  void _visualAreaChanged(sender, IVisualArea e) {
+    if (mounted) {
+      _setMeasureArea(e);
+    }
+  }
+
+  void _setMeasureArea(IVisualArea e) {
+    if (e.visualAreaType == VidUsVisualAreaType.TissueTimeMotion ||
+        e.visualAreaType == VidUsVisualAreaType.Doppler) {
+      regionHeight = (1 - e.displayRegion.top) * context.size!.height;
+      regionWidth = context.size!.width;
+      regionTop = e.displayRegion.top * context.size!.height;
+      regionLeft = 0;
+    } else {
+      regionHeight = e.displayRegion.height * context.size!.height;
+      regionWidth = e.displayRegion.width * context.size!.width;
+      regionTop = e.displayRegion.top * context.size!.height;
+      regionLeft = e.displayRegion.left * context.size!.width;
+    }
+    setState(() {});
+  }
+
+  void _initVisualArea(IVisualArea e) {
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      _setMeasureArea(e);
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return IgnorePointer(
+      child: Stack(
+        children: [
+          Positioned(
+            top: regionTop,
+            left: regionLeft,
+            child: Container(
+              width: regionWidth,
+              height: regionHeight,
+              decoration: BoxDecoration(
+                border: Border.all(
+                  strokeAlign: BorderSide.strokeAlignCenter,
+                  color: Colors.grey.withOpacity(0.5),
+                  width: 1,
+                ),
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}