284 lines
11 KiB
Python
284 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
Step1View —— Step 1(水域掩膜生成)的前端视图(端到端模块化)
|
||
|
||
设计原则
|
||
========
|
||
|
||
1. **UI 完全移植**:控件、布局、单选样式、信号槽、文件过滤器、阈值参数
|
||
全部从 ``src/gui/panels/step1_panel.py`` 原样搬迁,业务行为保持一致。
|
||
2. **零业务逻辑**:本文件 **绝不** import 任何 ``src/core/``、``src/services/``
|
||
算法模块;**绝不** 调用 Pipeline;唯一跨层出口是底部按钮的
|
||
``self.dispatch_execute("step1", self.get_config())``。
|
||
3. **契约对齐**:继承 ``BaseView``;实现 ``init_ui / get_config / set_config``;
|
||
通过 ``update_work_directory`` 在 NDWI 模式下自动填充输出路径。
|
||
|
||
调用链(自顶向下)::
|
||
|
||
Step1View._on_run_clicked
|
||
→ BaseView.dispatch_execute("step1", config)
|
||
→ MainView.run_single_step(step_id, config)
|
||
→ TaskWorker → services.step1_service.execute_step1(config)
|
||
→ 返回结果 dict → MainView 日志区
|
||
"""
|
||
|
||
import os
|
||
|
||
from PyQt5.QtCore import Qt
|
||
from PyQt5.QtWidgets import (
|
||
QCheckBox, QDoubleSpinBox, QGroupBox, QHBoxLayout,
|
||
QLabel, QPushButton, QRadioButton, QVBoxLayout,
|
||
)
|
||
|
||
from src.gui.components.custom_widgets import FileSelectWidget
|
||
from src.gui.styles import ModernStylesheet
|
||
from src.new.core.base_view import BaseView
|
||
|
||
|
||
# QRadioButton 统一样式(实心选中点)——与旧 panel 完全一致
|
||
_RADIO_STYLE = """
|
||
QRadioButton::indicator {
|
||
width: 16px;
|
||
height: 16px;
|
||
border-radius: 8px;
|
||
border: 2px solid #999;
|
||
}
|
||
QRadioButton::indicator:checked {
|
||
background-color: #0078D7;
|
||
border: 2px solid #0078D7;
|
||
}
|
||
QRadioButton::indicator:unchecked {
|
||
background-color: white;
|
||
border: 2px solid #999;
|
||
}
|
||
QRadioButton::indicator:hover {
|
||
border: 2px solid #0078D7;
|
||
}
|
||
"""
|
||
|
||
|
||
class Step1View(BaseView):
|
||
"""Step 1: 水域掩膜生成 —— 纯 View,所有跨层通讯走 dispatch_execute"""
|
||
|
||
# ------------------------------------------------------------------
|
||
# UI 构建(与旧 step1_panel.init_ui 字节级一致,仅按钮文案与回调不同)
|
||
# ------------------------------------------------------------------
|
||
def init_ui(self):
|
||
layout = QVBoxLayout()
|
||
|
||
# ---------- 掩膜生成方式 ----------
|
||
method_group = QGroupBox("掩膜生成方式")
|
||
method_layout = QVBoxLayout()
|
||
|
||
self.use_existing_radio = QRadioButton("使用现有掩膜文件")
|
||
self.use_existing_radio.setChecked(True)
|
||
method_layout.addWidget(self.use_existing_radio)
|
||
|
||
self.use_ndwi_radio = QRadioButton("使用NDWI自动生成")
|
||
method_layout.addWidget(self.use_ndwi_radio)
|
||
|
||
self.use_existing_radio.setStyleSheet(_RADIO_STYLE)
|
||
self.use_ndwi_radio.setStyleSheet(_RADIO_STYLE)
|
||
|
||
method_group.setLayout(method_layout)
|
||
layout.addWidget(method_group)
|
||
|
||
# ---------- 掩膜文件 ----------
|
||
self.mask_file = FileSelectWidget(
|
||
"掩膜文件:",
|
||
"Shapefiles (*.shp);;Raster Files (*.dat *.tif);;All Files (*.*)"
|
||
)
|
||
layout.addWidget(self.mask_file)
|
||
|
||
# ---------- 参考影像 ----------
|
||
self.img_file = FileSelectWidget(
|
||
"参考影像:",
|
||
"Image Files (*.bsq *.dat *.tif);;All Files (*.*)"
|
||
)
|
||
layout.addWidget(self.img_file)
|
||
|
||
# ---------- NDWI 参数 ----------
|
||
self.ndwi_group = QGroupBox("NDWI参数设置")
|
||
ndwi_layout = QVBoxLayout()
|
||
|
||
threshold_layout = QHBoxLayout()
|
||
threshold_layout.addWidget(QLabel("NDWI阈值:"))
|
||
self.ndwi_threshold = QDoubleSpinBox()
|
||
self.ndwi_threshold.setRange(0.0, 1.0)
|
||
self.ndwi_threshold.setSingleStep(0.05)
|
||
self.ndwi_threshold.setValue(0.4)
|
||
self.ndwi_threshold.setDecimals(2)
|
||
threshold_layout.addWidget(self.ndwi_threshold)
|
||
threshold_layout.addStretch()
|
||
ndwi_layout.addLayout(threshold_layout)
|
||
|
||
self.ndwi_group.setLayout(ndwi_layout)
|
||
layout.addWidget(self.ndwi_group)
|
||
|
||
# ---------- 输出掩膜路径(save 模式) ----------
|
||
self.output_file = FileSelectWidget(
|
||
"输出掩膜:",
|
||
"Mask Files (*.dat *.tif);;All Files (*.*)",
|
||
mode="save",
|
||
)
|
||
self.output_file.line_edit.setPlaceholderText("water_mask.dat")
|
||
layout.addWidget(self.output_file)
|
||
|
||
# ---------- 提示信息(Info Alert 样式) ----------
|
||
hint = QLabel(
|
||
"💡 提示: 如果掩膜文件是Shapefile(.shp),需要提供参考影像用于栅格化;"
|
||
"如果使用NDWI自动生成,只需要提供参考影像"
|
||
)
|
||
hint.setWordWrap(True)
|
||
hint.setStyleSheet("""
|
||
QLabel {
|
||
color: #0055D4;
|
||
font-size: 13px;
|
||
font-weight: bold;
|
||
background-color: #E8F4FF;
|
||
border: 2px solid #0055D4;
|
||
border-radius: 8px;
|
||
padding: 12px 16px;
|
||
margin: 8px 0px;
|
||
}
|
||
""")
|
||
layout.addWidget(hint)
|
||
|
||
# ---------- 启用步骤 ----------
|
||
self.enable_checkbox = QCheckBox("启用此步骤")
|
||
self.enable_checkbox.setChecked(True)
|
||
layout.addWidget(self.enable_checkbox)
|
||
|
||
# ---------- 执行按钮(绿色 success 样式,仅作视图层入口) ----------
|
||
self.run_btn = QPushButton("执行 Step 1: 水域掩膜")
|
||
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet("success"))
|
||
self.run_btn.clicked.connect(self._on_run_clicked)
|
||
layout.addWidget(self.run_btn)
|
||
|
||
# ---------- 信号联动 ----------
|
||
self.use_existing_radio.toggled.connect(self.update_ui_state)
|
||
self.use_ndwi_radio.toggled.connect(self.update_ui_state)
|
||
|
||
layout.addStretch()
|
||
self.setLayout(layout)
|
||
|
||
# 初始 UI 状态
|
||
self.update_ui_state()
|
||
|
||
# ------------------------------------------------------------------
|
||
# UI 状态联动(与旧 panel.update_ui_state 同语义)
|
||
# ------------------------------------------------------------------
|
||
def update_ui_state(self):
|
||
"""根据单选状态显示/隐藏 NDWI 参数与输出路径"""
|
||
use_ndwi = self.use_ndwi_radio.isChecked()
|
||
|
||
if use_ndwi:
|
||
self.mask_file.setVisible(False)
|
||
self.ndwi_group.setVisible(True)
|
||
self.output_file.setVisible(True)
|
||
|
||
# 主窗口已推送过 work_dir 才填,避免 init_ui 阶段空指针
|
||
if self.work_dir:
|
||
self._auto_fill_output_path()
|
||
else:
|
||
self.mask_file.setVisible(True)
|
||
self.ndwi_group.setVisible(False)
|
||
self.output_file.setVisible(False)
|
||
|
||
# 参考影像在两种模式下都显示
|
||
self.img_file.setVisible(True)
|
||
|
||
# ------------------------------------------------------------------
|
||
# 工作目录推送——主窗口变更 work_dir 时调用
|
||
# ------------------------------------------------------------------
|
||
def update_work_directory(self, work_dir):
|
||
"""主窗口推送工作目录变更;NDWI 模式下自动填充输出路径
|
||
|
||
重写基类以叠加"自动填路径"行为,再 super 缓存保持一致。
|
||
"""
|
||
super().update_work_directory(work_dir)
|
||
|
||
if work_dir and self.use_ndwi_radio.isChecked():
|
||
self._auto_fill_output_path()
|
||
|
||
def _auto_fill_output_path(self):
|
||
"""NDWI 模式下根据 work_dir 推导默认输出路径(统一正斜杠)
|
||
|
||
view 层内联的轻量推导——不依赖旧 ``_step_path_resolver``:
|
||
output_dir = work_dir/1_water_mask/
|
||
default_output = output_dir/water_mask_out.dat
|
||
"""
|
||
if not self.work_dir:
|
||
return
|
||
|
||
output_dir = os.path.join(self.work_dir, "1_water_mask")
|
||
os.makedirs(output_dir, exist_ok=True)
|
||
|
||
default_output_path = os.path.join(output_dir, "water_mask_out.dat").replace("\\", "/")
|
||
self.output_file.set_path(default_output_path)
|
||
|
||
# ------------------------------------------------------------------
|
||
# BaseView 契约:get_config / set_config
|
||
# ------------------------------------------------------------------
|
||
def get_config(self) -> dict:
|
||
"""读取当前 UI 状态——返回干净字典供 dispatch_execute 携带
|
||
|
||
字段:
|
||
mask_path : str | None 现有掩膜文件路径;NDWI 模式下为 None
|
||
use_ndwi : bool 是否走 NDWI 自动生成
|
||
ndwi_threshold : float NDWI 阈值
|
||
img_path : str 参考影像路径(非空才包含)
|
||
output_path : str | None NDWI 模式下的输出路径;现有掩膜模式下为 None
|
||
"""
|
||
use_ndwi = self.use_ndwi_radio.isChecked()
|
||
|
||
config = {
|
||
"mask_path": None if use_ndwi else self.mask_file.get_path(),
|
||
"use_ndwi": use_ndwi,
|
||
"ndwi_threshold": self.ndwi_threshold.value(),
|
||
}
|
||
|
||
# 参考影像:非空才写入,避免下游合并时被空字符串覆盖
|
||
img_path = self.img_file.get_path()
|
||
if img_path:
|
||
config["img_path"] = img_path
|
||
|
||
if use_ndwi:
|
||
output_path = self.output_file.get_path()
|
||
if output_path:
|
||
config["output_path"] = output_path
|
||
else:
|
||
# 现有掩膜模式下不传递 output_path,避免底层错误尝试保存文件
|
||
config["output_path"] = None
|
||
|
||
return config
|
||
|
||
def set_config(self, config: dict):
|
||
"""根据 config 恢复 UI 状态——None / 空值字段一律跳过"""
|
||
if config.get("mask_path"):
|
||
self.mask_file.set_path(config["mask_path"])
|
||
if config.get("img_path"):
|
||
self.img_file.set_path(config["img_path"])
|
||
if config.get("output_path"):
|
||
self.output_file.set_path(config["output_path"])
|
||
if "use_ndwi" in config:
|
||
if config["use_ndwi"]:
|
||
self.use_ndwi_radio.setChecked(True)
|
||
else:
|
||
self.use_existing_radio.setChecked(True)
|
||
if "ndwi_threshold" in config:
|
||
self.ndwi_threshold.setValue(config["ndwi_threshold"])
|
||
|
||
self.update_ui_state()
|
||
|
||
# ------------------------------------------------------------------
|
||
# 执行入口:仅触发 dispatch_execute——无任何业务校验/算法调用
|
||
# ------------------------------------------------------------------
|
||
def _on_run_clicked(self):
|
||
"""绿色按钮:纯 View 层入口,把当前 UI 配置打包交给主窗口
|
||
|
||
所有跨层通讯一律走 ``dispatch_execute``:
|
||
|
||
Step1View → 沿父链上溯 → MainView.run_single_step(step_id, config)
|
||
"""
|
||
self.dispatch_execute("step1", self.get_config()) |