From da8d1f178e2433bdf4bc10e50e89f697d6bf8571 Mon Sep 17 00:00:00 2001 From: copper Date: Thu, 12 May 2022 15:38:56 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E8=81=94=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/basic_change/main.py | 102 +++++++++++---- pyinstaller.help | 247 ----------------------------------- rscder/gui/__init__.py | 0 rscder/gui/layercombox.py | 22 ++++ rscder/gui/layertree.py | 170 ++++++++++++++++++++---- rscder/gui/mainwindow.py | 5 + rscder/gui/mapcanvas.py | 51 ++++++-- rscder/gui/result.py | 25 +++- rscder/utils/project.py | 227 ++++++++++++++++++++++++++++---- 9 files changed, 507 insertions(+), 342 deletions(-) delete mode 100644 pyinstaller.help create mode 100644 rscder/gui/__init__.py create mode 100644 rscder/gui/layercombox.py diff --git a/plugins/basic_change/main.py b/plugins/basic_change/main.py index c6a6397..1847bca 100644 --- a/plugins/basic_change/main.py +++ b/plugins/basic_change/main.py @@ -2,16 +2,60 @@ import math import os import pdb from rscder.plugins.basic import BasicPlugin -from PyQt5.QtWidgets import QAction +from PyQt5.QtWidgets import QAction, QDialog, QHBoxLayout, QVBoxLayout, QPushButton from PyQt5.QtCore import pyqtSignal -from rscder.utils.project import PairLayer, ResultLayer +from PyQt5.QtGui import QIcon +from rscder.utils.project import Project, PairLayer, ResultLayer +from rscder.gui.layercombox import LayerCombox from osgeo import gdal, gdal_array from threading import Thread import numpy as np + +class MyDialog(QDialog): + + def __init__(self, parent=None): + super().__init__(parent) + + self.setWindowTitle('BasicChange') + self.setWindowIcon(QIcon(":/icons/logo.svg")) + + self.setFixedWidth(500) + + self.layer_select = LayerCombox(self) + self.layer_select.setFixedWidth(400) + + # self.number_input = QLineEdit(self) + + self.ok_button = QPushButton('OK', self) + self.ok_button.setIcon(QIcon(":/icons/ok.svg")) + self.ok_button.clicked.connect(self.on_ok) + + self.cancel_button = QPushButton('Cancel', self) + self.cancel_button.setIcon(QIcon(":/icons/cancel.svg")) + self.cancel_button.clicked.connect(self.on_cancel) + + self.button_layout = QHBoxLayout() + self.button_layout.addWidget(self.ok_button) + self.button_layout.addWidget(self.cancel_button) + + self.main_layout = QVBoxLayout() + self.main_layout.addWidget(self.layer_select) + # self.main_layout.addWidget(self.number_input) + self.main_layout.addLayout(self.button_layout) + self.setLayout(self.main_layout) + + def on_ok(self): + self.accept() + + def on_cancel(self): + self.reject() + + + class BasicMethod(BasicPlugin): message_send = pyqtSignal(str) - table_result_ok = pyqtSignal(str) + result_ok = pyqtSignal(dict) @staticmethod def info(): @@ -33,34 +77,31 @@ class BasicMethod(BasicPlugin): basic_diff_method.triggered.connect(self.basic_diff_alg) self.message_send.connect(self.send_message) - self.table_result_ok.connect(self.on_table_result_ok) - + self.result_ok.connect(self.on_result_ok) + # self.result_ok.connect(self.on_result_ok) self.gap = 128 + def on_data_load(self, layer_id): self.basic_diff_method.setEnabled(True) def send_message(self, s): self.message_box.info(s) - def on_table_result_ok(self, s): - with open(s, 'r') as f: - lines = f.readlines() - data_lines = lines[1:] + def on_result_ok(self, data): + layer = Project().layers[data['layer_id']] + csv_result = ResultLayer('basic_diff_result', ResultLayer.POINT) + csv_result.load_file(data['csv_file']) + layer.results.append(csv_result) + self.layer_tree.update_layer(layer.id) + + def run_basic_diff_alg(self, layer:PairLayer, out): - if len(data_lines) > 0: - data_table = [] - for l in data_lines: - l = l.strip() - ls = l.split(',') - ls = [float(i) for i in ls] - data_table.append(ls) - result = ResultLayer(ResultLayer.POINT) - result.data = data_table - self.result_table.set_data(result) + pth1 = layer.pth1 + pth2 = layer.pth2 + cell_size = layer.cell_size - def run_basic_diff_alg(self, pth1, pth2, cell_size, out): self.message_send.emit('开始计算差分法') ds1 = gdal.Open(pth1) @@ -125,7 +166,7 @@ class BasicMethod(BasicPlugin): self.message_send.emit('完成归一化概率') self.message_send.emit('计算变化表格中...') - out_csv = os.path.join(out, 'diff_table.csv') + out_csv = os.path.join(out, '{}.csv'.format(int(np.random.rand() * 100000))) xblocks = xsize // cell_size[0] normal_in_ds = gdal.Open(out_normal_tif) @@ -151,7 +192,11 @@ class BasicMethod(BasicPlugin): center_y = center_y * geo[5] + geo [3] f.write(f'{center_x},{center_y},{block_data_xy.mean()},1\n') - self.table_result_ok.emit(out_csv) + + self.result_ok.emit({ + 'layer_id': layer.id, + 'csv_file': out_csv, + }) self.message_send.emit('完成计算变化表格') @@ -159,10 +204,13 @@ class BasicMethod(BasicPlugin): def basic_diff_alg(self): # layer_select = - layer:PairLayer = list(self.project.layers.values())[0] - - img1 = layer.pth1 - img2 = layer.pth2 + layer = None + layer_select = MyDialog(self.mainwindow) + if(layer_select.exec_()): + layer = layer_select.layer_select.current_layer + else: + return + # layer:PairLayer = list(self.project.layers.values())[0] if not layer.check(): return @@ -170,7 +218,7 @@ class BasicMethod(BasicPlugin): if not os.path.exists(out_dir): os.makedirs(out_dir, exist_ok=True) - t = Thread(target=self.run_basic_diff_alg, args=(img1, img2, layer.cell_size, out_dir)) + t = Thread(target=self.run_basic_diff_alg, args=(layer, out_dir)) t.start() diff --git a/pyinstaller.help b/pyinstaller.help deleted file mode 100644 index b52d023..0000000 --- a/pyinstaller.help +++ /dev/null @@ -1,247 +0,0 @@ -usage: pyinstaller [-h] [-v] [-D] [-F] [--specpath DIR] [-n NAME] - [--add-data ] - [--add-binary ] [-p DIR] - [--hidden-import MODULENAME] - [--collect-submodules MODULENAME] - [--collect-data MODULENAME] [--collect-binaries MODULENAME] - [--collect-all MODULENAME] [--copy-metadata PACKAGENAME] - [--recursive-copy-metadata PACKAGENAME] - [--additional-hooks-dir HOOKSPATH] - [--runtime-hook RUNTIME_HOOKS] [--exclude-module EXCLUDES] - [--key KEY] [--splash IMAGE_FILE] - [-d {all,imports,bootloader,noarchive}] [-s] [--noupx] - [--upx-exclude FILE] [-c] [-w] - [-i ] - [--disable-windowed-traceback] [--version-file FILE] - [-m ] [-r RESOURCE] [--uac-admin] - [--uac-uiaccess] [--win-private-assemblies] - [--win-no-prefer-redirects] - [--osx-bundle-identifier BUNDLE_IDENTIFIER] - [--target-architecture ARCH] [--codesign-identity IDENTITY] - [--osx-entitlements-file FILENAME] [--runtime-tmpdir PATH] - [--bootloader-ignore-signals] [--distpath DIR] - [--workpath WORKPATH] [-y] [--upx-dir UPX_DIR] [-a] - [--clean] [--log-level LEVEL] - scriptname [scriptname ...] - -positional arguments: - scriptname name of scriptfiles to be processed or exactly one - .spec-file. If a .spec-file is specified, most options - are unnecessary and are ignored. - -optional arguments: - -h, --help show this help message and exit - -v, --version Show program version info and exit. - --distpath DIR Where to put the bundled app (default: .\dist) - --workpath WORKPATH Where to put all the temporary work files, .log, .pyz - and etc. (default: .\build) - -y, --noconfirm Replace output directory (default: - SPECPATH\dist\SPECNAME) without asking for - confirmation - --upx-dir UPX_DIR Path to UPX utility (default: search the execution - path) - -a, --ascii Do not include unicode encoding support (default: - included if available) - --clean Clean PyInstaller cache and remove temporary files - before building. - --log-level LEVEL Amount of detail in build-time console messages. LEVEL - may be one of TRACE, DEBUG, INFO, WARN, ERROR, - CRITICAL (default: INFO). - -What to generate: - -D, --onedir Create a one-folder bundle containing an executable - (default) - -F, --onefile Create a one-file bundled executable. - --specpath DIR Folder to store the generated spec file (default: - current directory) - -n NAME, --name NAME Name to assign to the bundled app and spec file - (default: first script's basename) - -What to bundle, where to search: - --add-data - Additional non-binary files or folders to be added to - the executable. The path separator is platform - specific, ``os.pathsep`` (which is ``;`` on Windows - and ``:`` on most unix systems) is used. This option - can be used multiple times. - --add-binary - Additional binary files to be added to the executable. - See the ``--add-data`` option for more details. This - option can be used multiple times. - -p DIR, --paths DIR A path to search for imports (like using PYTHONPATH). - Multiple paths are allowed, separated by ``';'``, or - use this option multiple times. Equivalent to - supplying the ``pathex`` argument in the spec file. - --hidden-import MODULENAME, --hiddenimport MODULENAME - Name an import not visible in the code of the - script(s). This option can be used multiple times. - --collect-submodules MODULENAME - Collect all submodules from the specified package or - module. This option can be used multiple times. - --collect-data MODULENAME, --collect-datas MODULENAME - Collect all data from the specified package or module. - This option can be used multiple times. - --collect-binaries MODULENAME - Collect all binaries from the specified package or - module. This option can be used multiple times. - --collect-all MODULENAME - Collect all submodules, data files, and binaries from - the specified package or module. This option can be - used multiple times. - --copy-metadata PACKAGENAME - Copy metadata for the specified package. This option - can be used multiple times. - --recursive-copy-metadata PACKAGENAME - Copy metadata for the specified package and all its - dependencies. This option can be used multiple times. - --additional-hooks-dir HOOKSPATH - An additional path to search for hooks. This option - can be used multiple times. - --runtime-hook RUNTIME_HOOKS - Path to a custom runtime hook file. A runtime hook is - code that is bundled with the executable and is - executed before any other code or module to set up - special features of the runtime environment. This - option can be used multiple times. - --exclude-module EXCLUDES - Optional module or package (the Python name, not the - path name) that will be ignored (as though it was not - found). This option can be used multiple times. - --key KEY The key used to encrypt Python bytecode. - --splash IMAGE_FILE (EXPERIMENTAL) Add an splash screen with the image - IMAGE_FILE to the application. The splash screen can - show progress updates while unpacking. - -How to generate: - -d {all,imports,bootloader,noarchive}, --debug {all,imports,bootloader,noarchive} - Provide assistance with debugging a frozen - application. This argument may be provided multiple - times to select several of the following options. - - - all: All three of the following options. - - - imports: specify the -v option to the underlying - Python interpreter, causing it to print a message - each time a module is initialized, showing the - place (filename or built-in module) from which it - is loaded. See - https://docs.python.org/3/using/cmdline.html#id4. - - - bootloader: tell the bootloader to issue progress - messages while initializing and starting the - bundled app. Used to diagnose problems with - missing imports. - - - noarchive: instead of storing all frozen Python - source files as an archive inside the resulting - executable, store them as files in the resulting - output directory. - - -s, --strip Apply a symbol-table strip to the executable and - shared libs (not recommended for Windows) - --noupx Do not use UPX even if it is available (works - differently between Windows and *nix) - --upx-exclude FILE Prevent a binary from being compressed when using upx. - This is typically used if upx corrupts certain - binaries during compression. FILE is the filename of - the binary without path. This option can be used - multiple times. - -Windows and Mac OS X specific options: - -c, --console, --nowindowed - Open a console window for standard i/o (default). On - Windows this option will have no effect if the first - script is a '.pyw' file. - -w, --windowed, --noconsole - Windows and Mac OS X: do not provide a console window - for standard i/o. On Mac OS X this also triggers - building an OS X .app bundle. On Windows this option - will be set if the first script is a '.pyw' file. This - option is ignored in *NIX systems. - -i , --icon - FILE.ico: apply that icon to a Windows executable. - FILE.exe,ID, extract the icon with ID from an exe. - FILE.icns: apply the icon to the .app bundle on Mac OS - X. Use "NONE" to not apply any icon, thereby making - the OS to show some default (default: apply - PyInstaller's icon) - --disable-windowed-traceback - Disable traceback dump of unhandled exception in - windowed (noconsole) mode (Windows and macOS only), - and instead display a message that this feature is - disabled. - -Windows specific options: - --version-file FILE add a version resource from FILE to the exe - -m , --manifest - add manifest FILE or XML to the exe - -r RESOURCE, --resource RESOURCE - Add or update a resource to a Windows executable. The - RESOURCE is one to four items, - FILE[,TYPE[,NAME[,LANGUAGE]]]. FILE can be a data file - or an exe/dll. For data files, at least TYPE and NAME - must be specified. LANGUAGE defaults to 0 or may be - specified as wildcard * to update all resources of the - given TYPE and NAME. For exe/dll files, all resources - from FILE will be added/updated to the final - executable if TYPE, NAME and LANGUAGE are omitted or - specified as wildcard *.This option can be used - multiple times. - --uac-admin Using this option creates a Manifest which will - request elevation upon application restart. - --uac-uiaccess Using this option allows an elevated application to - work with Remote Desktop. - -Windows Side-by-side Assembly searching options (advanced): - --win-private-assemblies - Any Shared Assemblies bundled into the application - will be changed into Private Assemblies. This means - the exact versions of these assemblies will always be - used, and any newer versions installed on user - machines at the system level will be ignored. - --win-no-prefer-redirects - While searching for Shared or Private Assemblies to - bundle into the application, PyInstaller will prefer - not to follow policies that redirect to newer - versions, and will try to bundle the exact versions of - the assembly. - -Mac OS X specific options: - --osx-bundle-identifier BUNDLE_IDENTIFIER - Mac OS X .app bundle identifier is used as the default - unique program name for code signing purposes. The - usual form is a hierarchical name in reverse DNS - notation. For example: - com.mycompany.department.appname (default: first - script's basename) - --target-architecture ARCH, --target-arch ARCH - Target architecture (macOS only; valid values: x86_64, - arm64, universal2). Enables switching between - universal2 and single-arch version of frozen - application (provided python installation supports the - target architecture). If not target architecture is - not specified, the current running architecture is - targeted. - --codesign-identity IDENTITY - Code signing identity (macOS only). Use the provided - identity to sign collected binaries and generated - executable. If signing identity is not provided, ad- - hoc signing is performed instead. - --osx-entitlements-file FILENAME - Entitlements file to use when code-signing the - collected binaries (macOS only). - -Rarely used special options: - --runtime-tmpdir PATH - Where to extract libraries and support files in - `onefile`-mode. If this option is given, the - bootloader will ignore any temp-folder location - defined by the run-time OS. The ``_MEIxxxxxx``-folder - will be created here. Please use this option only if - you know what you are doing. - --bootloader-ignore-signals - Tell the bootloader to ignore signals rather than - forwarding them to the child process. Useful in - situations where e.g. a supervisor process signals - both the bootloader and child (e.g. via a process - group) to avoid signalling the child twice. diff --git a/rscder/gui/__init__.py b/rscder/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rscder/gui/layercombox.py b/rscder/gui/layercombox.py new file mode 100644 index 0000000..8e6b2a6 --- /dev/null +++ b/rscder/gui/layercombox.py @@ -0,0 +1,22 @@ +from PyQt5.QtWidgets import QComboBox +from rscder.utils.project import Project +class LayerCombox(QComboBox): + + def __init__(self, parent=None): + super().__init__(parent) + + self.addItem('---', None) + + for layer in Project().layers.values(): + self.addItem(layer.name, layer.id) + + self.currentIndexChanged.connect(self.on_changed) + + self.current_layer = None + + def on_changed(self, index): + if index == 0: + self.current_layer = None + else: + self.current_layer = Project().layers[self.itemData(index)] + diff --git a/rscder/gui/layertree.py b/rscder/gui/layertree.py index d933ba9..4e7a0d4 100644 --- a/rscder/gui/layertree.py +++ b/rscder/gui/layertree.py @@ -1,7 +1,8 @@ +import pdb from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtCore import Qt,QModelIndex -from PyQt5.QtGui import QStandardItemModel, QStandardItem +from PyQt5.QtGui import QStandardItemModel, QStandardItem, QCursor from PyQt5.QtWidgets import (QTreeView, QTreeWidgetItem, QAbstractItemView, QHeaderView, QStyleFactory) from rscder.gui.actions import get_action_manager @@ -9,6 +10,15 @@ from rscder.utils.project import PairLayer, Project class LayerTree(QtWidgets.QWidget): + LAYER_TOOT = 0 + SUB_RASTER = 1 + RESULT = 2 + LEFT_RASTER = 0 + RIGHT_RASTER = 1 + GRID = 3 + + tree_changed = QtCore.pyqtSignal(str) + result_clicked = QtCore.pyqtSignal(str, int) def __init__(self, parent=None): super().__init__(parent) # self.tree_view = QTreeView(self) @@ -16,7 +26,8 @@ class LayerTree(QtWidgets.QWidget): self.tree.setColumnCount(1) self.setContextMenuPolicy(Qt.CustomContextMenu) - self.customContextMenuRequested.connect(self.right_menu_show) + self.tree.setContextMenuPolicy(Qt.CustomContextMenu) + self.tree.customContextMenuRequested.connect(self.right_menu_show) self.root=QTreeWidgetItem(self.tree) self.tree.setHeaderHidden(True) # self.tree.setHeaderLabels(['图层']) @@ -31,35 +42,122 @@ class LayerTree(QtWidgets.QWidget): self.tree.addTopLevelItem(self.root) - self.tree.clicked.connect(self.onClicked) + # self.tree.clicked.connect(self.onClicked) + self.tree.itemClicked.connect(self.onItemClicked) + self.tree.itemChanged.connect(self.onItemChanged) layout = QtWidgets.QGridLayout() layout.addWidget(self.tree) self.setLayout(layout) self.setLayoutDirection(Qt.LeftToRight) + self.is_in_add_layer = False - def onClicked(self,index): - print(index.row()) - item = self.tree.currentItem() + def onItemClicked(self, item:QtWidgets.QTreeWidgetItem, column): if item == self.root: return - layer_id = str(item.data(0, Qt.UserRole)) - layer = Project().layers[layer_id] - print(layer.l1_name) - print(layer.l2_name) + root = item + if item.data(0, Qt.UserRole) != LayerTree.LAYER_TOOT: + root = item.parent() + if item.data(0, Qt.UserRole) == LayerTree.LAYER_TOOT: + return + layer = Project().layers[root.data(0, Qt.UserRole + 1)] + Project().current_layer = layer + if item.data(0, Qt.UserRole) == LayerTree.RESULT: + # result = layer.results[item.data(0, Qt.UserRole + 1)] + self.result_clicked.emit(layer.id, item.data(0, Qt.UserRole + 1)) + + + def onItemChanged(self, item:QtWidgets.QTreeWidgetItem, column): + if self.is_in_add_layer: + return + if item == self.root: + return + root = item + if item.data(0, Qt.UserRole) != LayerTree.LAYER_TOOT: + root = item.parent() + + layer = Project().layers[root.data(0, Qt.UserRole + 1)] + if item.data(0, Qt.UserRole) == LayerTree.LAYER_TOOT: + layer.enable = item.checkState(0) == Qt.Checked + if item.data(0, Qt.UserRole) == LayerTree.SUB_RASTER: + if item.data(0, Qt.UserRole + 1) == LayerTree.LEFT_RASTER: + layer.l1_enable = item.checkState(0) == Qt.Checked + elif item.data(0, Qt.UserRole + 1) == LayerTree.RIGHT_RASTER: + layer.l2_enable = item.checkState(0) == Qt.Checked + if item.data(0, Qt.UserRole) == LayerTree.RESULT: + layer.results[item.data(0, Qt.UserRole + 1)].enable = item.checkState(0) == Qt.Checked + + if item.data(0, Qt.UserRole) == LayerTree.GRID: + layer.grid_enable = item.checkState(0) == Qt.Checked + + self.tree_changed.emit(layer.id) def add_layer(self, layer:str): + # self.tree.it + self.is_in_add_layer = True layer:PairLayer = Project().layers[layer] - item1 = QtWidgets.QTreeWidgetItem(self.root) - item1.setText(0, layer.l1_name) - item1.setCheckState(0, Qt.Checked) - item2 = QtWidgets.QTreeWidgetItem(self.root) - item2.setText(0, layer.l2_name) - item2.setCheckState(0, Qt.Checked) + item_root = QtWidgets.QTreeWidgetItem(self.root) + item_root.setText(0,layer.name) + item_root.setData(0, Qt.UserRole, LayerTree.LAYER_TOOT) + item_root.setData(0, Qt.UserRole + 1, layer.id) + item_root.setCheckState(0, Qt.Checked if layer.enable else Qt.Unchecked) - item1.setData(0, Qt.UserRole, layer.id) - item2.setData(0, Qt.UserRole, layer.id) + self.add_sub_layer(item_root, layer) + self.is_in_add_layer = False + + def add_sub_layer(self, item_root, layer:PairLayer): + # print(item_root.text(0)) + # print(layer.results.__len__()) + grid_item = QtWidgets.QTreeWidgetItem(item_root) + grid_item.setText(0,'格网') + grid_item.setData(0, Qt.UserRole, LayerTree.GRID) + grid_item.setCheckState(0, Qt.Checked if layer.grid_enable else Qt.Unchecked) + + item1 = QtWidgets.QTreeWidgetItem(item_root) + item1.setText(0, layer.l1_name) + item1.setCheckState(0, Qt.Checked if layer.l1_enable else Qt.Unchecked) + item1.setData(0, Qt.UserRole, LayerTree.SUB_RASTER) + item1.setData(0, Qt.UserRole + 1, LayerTree.LEFT_RASTER) + + item2 = QtWidgets.QTreeWidgetItem(item_root) + item2.setText(0, layer.l2_name) + item2.setCheckState(0, Qt.Checked if layer.l2_enable else Qt.Unchecked) + item1.setData(0, Qt.UserRole, LayerTree.SUB_RASTER) + item1.setData(0, Qt.UserRole + 1, LayerTree.RIGHT_RASTER) + + for ri, item in enumerate(layer.results): + item_result = QtWidgets.QTreeWidgetItem(item_root) + item_result.setText(0, item.name) + item_result.setCheckState(0, Qt.Checked if item.enable else Qt.Unchecked) + item_result.setData(0, Qt.UserRole, LayerTree.RESULT) + item_result.setData(0, Qt.UserRole + 1, ri) + self.tree.expandAll() + + def update_layer(self, layer:str): + self.is_in_add_layer = True + layer:PairLayer = Project().layers[layer] + + layer_root = None + # pdb.set_trace() + for idx in range(self.root.childCount()): + item_root = self.root.child(idx) + if item_root.data(0, Qt.UserRole) == LayerTree.LAYER_TOOT: + if item_root.data(0, Qt.UserRole + 1) == layer.id: + layer_root = item_root + break + print(layer_root.text(0)) + if layer_root is None: + self.add_layer(layer.id) + return + + layer_root.setText(0,layer.name) + + while layer_root.childCount() > 0: + layer_root.removeChild(layer_root.child(0)) + + self.add_sub_layer(layer_root, layer) + self.is_in_add_layer = False def clear(self): self.tree.clear() @@ -70,17 +168,37 @@ class LayerTree(QtWidgets.QWidget): def right_menu_show(self, position): rightMenu = QtWidgets.QMenu(self) # QAction = QtWidgets.QAction(self.menuBar1) - item = self.tree.currentItem() + item = self.tree.itemAt(position) + action_manager = get_action_manager() actions = [] - if item == self.root: - data_load_action = action_manager.get_action('&数据加载', 'File') - actions.append(data_load_action) + data_load_action = action_manager.get_action('&数据加载', 'File') + actions.append(data_load_action) + if item is None: + print('nothing') else: - pass - + if item == self.root: + pass + elif item.data(0, Qt.UserRole) == LayerTree.LAYER_TOOT: + actions.append(QtWidgets.QAction('&缩放至该图层', self)) + + actions.append(QtWidgets.QAction('&重命名', self)) + actions.append(QtWidgets.QAction('&删除', self)) + elif item.data(0, Qt.UserRole) == LayerTree.SUB_RASTER: + actions.append(QtWidgets.QAction('&缩放至该图层', self)) + + actions.append(QtWidgets.QAction('&重命名', self)) + actions.append(QtWidgets.QAction('&删除', self)) + elif item.data(0, Qt.UserRole) == LayerTree.RESULT: + actions.append(QtWidgets.QAction('&缩放至该图层', self)) + + actions.append(QtWidgets.QAction('&重命名', self)) + actions.append(QtWidgets.QAction('&导出', self)) + actions.append(QtWidgets.QAction('&删除', self)) + + + for action in actions: rightMenu.addAction(action) - rightMenu.exec_(self.mapToGlobal(position)) - + rightMenu.exec_(QCursor.pos()) \ No newline at end of file diff --git a/rscder/gui/mainwindow.py b/rscder/gui/mainwindow.py index 1ad0873..b90a2ae 100644 --- a/rscder/gui/mainwindow.py +++ b/rscder/gui/mainwindow.py @@ -33,6 +33,11 @@ class MainWindow(QMainWindow): self.layer_tree, self.message_box, self.result_box) + self.layer_tree.tree_changed.connect(self.double_map.layer_changed) + self.layer_tree.result_clicked.connect(self.result_box.on_result) + self.result_box.on_item_click.connect(self.double_map.zoom_to_result) + self.result_box.on_item_changed.connect(Project().change_result) + self.action_manager = ActionManager( self.double_map, self.layer_tree, diff --git a/rscder/gui/mapcanvas.py b/rscder/gui/mapcanvas.py index ef80462..ec38b12 100644 --- a/rscder/gui/mapcanvas.py +++ b/rscder/gui/mapcanvas.py @@ -66,8 +66,9 @@ class DoubleCanvas(QWidget): action.setChecked(self.grid_show) if self.grid_show: for layer in Project().layers.values(): - self.mapcanva1.add_grid_layer(layer.grid_layer.grid_layer) - self.mapcanva2.add_grid_layer(layer.grid_layer.grid_layer) + if layer.grid_enable: + self.mapcanva1.add_grid_layer(layer.grid_layer.grid_layer) + self.mapcanva2.add_grid_layer(layer.grid_layer.grid_layer) else: self.mapcanva1.remove_grid_layer() self.mapcanva2.remove_grid_layer() @@ -99,16 +100,48 @@ class DoubleCanvas(QWidget): self.mapcanva2.setMapTool(QgsMapToolZoom(self.mapcanva2, True)) def add_layer(self, layer:str): - layer = Project().layers[layer] - if not self.mapcanva1.is_main and not self.mapcanva2.is_main: + layer:PairLayer = Project().layers[layer] + if not layer.enable: + return + self.clear() + + if not self.mapcanva1.is_main and not self.mapcanva2.is_main: self.mapcanva1.is_main = True - self.mapcanva1.add_layer(layer.l1) - self.mapcanva2.add_layer(layer.l2) - if self.grid_show: + + if layer.l1_enable: + self.mapcanva1.add_layer(layer.l1) + if layer.l2_enable: + self.mapcanva2.add_layer(layer.l2) + if layer.grid_enable and self.grid_show: self.mapcanva1.add_grid_layer(layer.grid_layer.grid_layer) self.mapcanva2.add_grid_layer(layer.grid_layer.grid_layer) - - + for r in layer.results: + if r.enable: + self.mapcanva1.add_layer(r.layer) + self.mapcanva2.add_layer(r.layer) + # self.mapcanva1.set_extent(layer.l1.extent()) + self.mapcanva1.refresh() + self.mapcanva2.refresh() + + def zoom_to_result(self, xydict:dict): + x = xydict['x'] + y = xydict['y'] + if Project().current_layer is not None: + layer = Project().current_layer + else: + layer = Project().layers[list(Project().layers.keys())[0]] + + extent = QgsRectangle(x - layer.cell_size[0] * layer.xres, y - layer.cell_size[1] * layer.yres, x + layer.cell_size[0] * layer.xres, y + layer.cell_size[1] * layer.yres) + self.mapcanva1.set_extent(extent) + self.mapcanva2.set_extent(extent) + + def zoom_to_layer(self, layer:str): + layer:PairLayer = Project().layers[layer] + self.mapcanva1.set_extent(layer.l1.extent()) + self.mapcanva2.set_extent(layer.l2.extent()) + def layer_changed(self, layer:str): + self.add_layer(layer) + def clear(self): self.mapcanva1.clear() self.mapcanva2.clear() diff --git a/rscder/gui/result.py b/rscder/gui/result.py index 4d522b9..f75cc8e 100644 --- a/rscder/gui/result.py +++ b/rscder/gui/result.py @@ -4,7 +4,7 @@ from PyQt5.QtCore import Qt,QModelIndex, pyqtSignal from PyQt5.QtGui import QStandardItemModel, QStandardItem from PyQt5.QtWidgets import (QTableWidgetItem, QTableWidget, QAbstractItemView, QHeaderView, QStyleFactory) -from rscder.utils.project import PairLayer, ResultLayer +from rscder.utils.project import PairLayer, Project, ResultLayer class ResultTable(QtWidgets.QWidget): @@ -29,11 +29,15 @@ class ResultTable(QtWidgets.QWidget): layout = QtWidgets.QVBoxLayout(self) layout.addWidget(self.tablewidget) self.setLayout(layout) + self.result = None + self.is_in_set_data = False def clear(self): - pass + self.tablewidget.clear() def onChanged(self, row, col): + if self.is_in_set_data: + return if col == 3: item_idx = row item_status = self.tablewidget.item(row, col).checkState() == Qt.Checked @@ -41,7 +45,8 @@ class ResultTable(QtWidgets.QWidget): self.tablewidget.item(row, col).setBackground(Qt.yellow) else: self.tablewidget.item(row, col).setBackground(Qt.green) - self.on_item_changed.emit({'idx':item_idx, 'status':item_status}) + # print(item_idx, item_status) + self.result.update({'row':item_idx, 'value':item_status}) def onClicked(self, row, col): if col == 3: @@ -50,9 +55,18 @@ class ResultTable(QtWidgets.QWidget): def onDoubleClicked(self, row, col): x = self.tablewidget.item(row, 0).text() y = self.tablewidget.item(row, 1).text() - self.on_item_click.emit({'x':x, 'y':y}) + self.on_item_click.emit({'x':float(x), 'y':float(y)}) + def on_result(self, layer_id, result_id): + self.is_in_set_data = True + result = Project().layers[layer_id].results[result_id] + self.result = result + self.clear() + self.set_data(result) def set_data(self, data:ResultLayer): + self.is_in_set_data = True + if data.layer_type != ResultLayer.POINT: + return self.tablewidget.setRowCount(len(data.data)) # print(len(data.data)) self.tablewidget.setVerticalHeaderLabels([ str(i+1) for i in range(len(data.data))]) @@ -73,5 +87,4 @@ class ResultTable(QtWidgets.QWidget): self.tablewidget.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.tablewidget.verticalHeader().setSectionResizeMode(QHeaderView.Stretch) - - + self.is_in_set_data = False \ No newline at end of file diff --git a/rscder/utils/project.py b/rscder/utils/project.py index c70b7eb..ae29f17 100644 --- a/rscder/utils/project.py +++ b/rscder/utils/project.py @@ -2,9 +2,10 @@ import os from pathlib import Path from typing import Dict, List import uuid +import numpy as np from osgeo import gdal, gdal_array from rscder.utils.setting import Settings -from qgis.core import QgsRasterLayer, QgsLineSymbol, QgsSingleSymbolRenderer, QgsSimpleLineSymbolLayer, QgsVectorLayer, QgsCoordinateReferenceSystem, QgsFeature, QgsGeometry, QgsPointXY +from qgis.core import QgsRasterLayer, QgsMarkerSymbol, QgsPalLayerSettings, QgsRuleBasedLabeling, QgsTextFormat, QgsLineSymbol, QgsSingleSymbolRenderer, QgsSimpleLineSymbolLayer, QgsVectorLayer, QgsCoordinateReferenceSystem, QgsFeature, QgsGeometry, QgsPointXY from PyQt5.QtCore import QObject, pyqtSignal from PyQt5.QtGui import QColor import yaml @@ -32,6 +33,7 @@ class Project(QObject): self.root = str(Path(Settings.General().root)/'default') self.file_mode = Project.ABSOLUTE_MODE self.layers:Dict[str, PairLayer] = dict() + self.current_layer = None def connect(self, pair_canvas, layer_tree, @@ -44,8 +46,17 @@ class Project(QObject): self.layer_load.connect(layer_tree.add_layer) self.layer_load.connect(pair_canvas.add_layer) - # self.layer_load.connect(message_box.add_layer) - + # self.layer_load.connect(message_box.add_layer) + # + def change_result(self, layer_id, result_id, data): + if layer_id in self.layers: + result = self.layers[layer_id].results[result_id] + + if result.layer_type == ResultLayer.POINT: + result.update(data) + elif result.layer_type == ResultLayer.RASTER: + pass + def setup(self, path = None, name = None): self.is_init = True if path is not None: @@ -71,7 +82,6 @@ class Project(QObject): 'max_threads': self.max_threads, 'root': self.root, 'layers': [ layer.to_dict(None if self.file_mode == Project.ABSOLUTE_MODE else self.root) for layer in self.layers.values() ], - 'results': [] } with open(self.file, 'w') as f: yaml.safe_dump(data_dict, f) @@ -89,22 +99,25 @@ class Project(QObject): self.result_table.clear() def load(self): - with open(self.file, 'r') as f: - data = yaml.safe_load(f) - if data is None: - return - # data = yaml.safe_load(open(self.file, 'r')) - self.cell_size = data['cell_size'] - self.max_memory = data['max_memory'] - self.max_threads = data['max_threads'] - self.root = data['root'] - self.layers = dict() - for layer in data['layers']: - player = PairLayer.from_dict(layer, None if self.file_mode == Project.ABSOLUTE_MODE else self.root) - if player.check(): - self.layers[player.id] = player - self.layer_load.emit(player.id) - + try: + with open(self.file, 'r') as f: + data = yaml.safe_load(f) + if data is None: + return + # data = yaml.safe_load(open(self.file, 'r')) + self.cell_size = data['cell_size'] + self.max_memory = data['max_memory'] + self.max_threads = data['max_threads'] + self.root = data['root'] + self.layers = dict() + for layer in data['layers']: + player = PairLayer.from_dict(layer, None if self.file_mode == Project.ABSOLUTE_MODE else self.root) + if player.check(): + self.layers[player.id] = player + self.layer_load.emit(player.id) + except Exception as e: + self.message_box.error(str(e)) + self.clear() def add_layer(self, pth1, pth2): # self.root = str(Path(pth1).parent) @@ -205,9 +218,142 @@ class ResultLayer: POINT = 0 RASTER = 1 - def __init__(self, layer_type): + def __init__(self, name, layer_type = POINT): self.layer_type = layer_type - self.data = [] + self.data = None + self.layer = None + self.name = name + self.path = None + self.wkt = None + self.enable = False + + def update(self, data): + if self.layer_type == ResultLayer.POINT: + row = data['row'] + value = data['value'] + self.data[row][-1] = value + self.update_point_layer() + elif self.layer_type == ResultLayer.RASTER: + pass + + def load_file(self, path): + self.path = path + if self.layer_type == ResultLayer.POINT: + self.load_point_file() + elif self.layer_type == ResultLayer.RASTER: + self.load_raster_file() + else: + raise Exception('Unknown layer type') + + def format_point_layer(self, layer): + layer.setLabelsEnabled(True) + lyr = QgsPalLayerSettings() + lyr.enabled = True + lyr.fieldName = 'id' + lyr.placement = QgsPalLayerSettings.OverPoint + lyr.textNamedStyle = 'Medium' + text_format = QgsTextFormat() + text_format.color = QColor('#ffffff') + text_format.background().color = QColor('#000000') + text_format.buffer().setEnabled(True) + text_format.buffer().setSize(1) + text_format.buffer().setOpacity(0.5) + lyr.setFormat(text_format) + root = QgsRuleBasedLabeling.Rule(QgsPalLayerSettings()) + rule = QgsRuleBasedLabeling.Rule(lyr) + rule.setDescription('label') + root.appendChild(rule) + #Apply label configuration + rules = QgsRuleBasedLabeling(root) + layer.setLabeling(rules) + + def set_render(self, layer): + symbol = QgsMarkerSymbol.createSimple({'color': '#ffffff', 'size': '5'}) + render = QgsSingleSymbolRenderer(symbol) + layer.setRenderer(render) + + def load_point_file(self): + data = np.loadtxt(self.path, delimiter=',', skiprows=1) + if data is None: + return + self.data = data + self.make_point_layer() + + def make_point_layer(self): + if self.wkt is not None: + crs = QgsCoordinateReferenceSystem() + crs.createFromString('WKT:{}'.format(self.wkt)) + else: + crs = QgsCoordinateReferenceSystem() + + uri = 'Point?crs={}'.format(crs.toProj()) + layer = QgsVectorLayer(uri, self.name, "memory") + if not layer.isValid(): + Project().message_box.error('Failed to create layer') + return + self.format_point_layer(layer) + layer.startEditing() + features = [] + for i, d in enumerate(self.data): + point = QgsFeature(i) + point.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(d[0], d[1]))) + # point.setAttribute('id', i) + features.append(point) + layer.addFeatures(features) + layer.commitChanges() + self.set_render(layer) + self.layer = layer + + def update_point_layer(self): + if self.layer is None: + return + self.layer.startEditing() + add_features = [] + delete_features = [] + for i, d in enumerate(self.data): + feature = self.layer.getFeature(i+1) + if d[-1]: + if feature is None: + feature = QgsFeature(i) + feature.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(d[0], d[1]))) + # feature.setAttribute('id', i) + add_features.append(feature) + else: + if feature is not None: + delete_features.append(feature.id()) + if len(add_features) > 0: + self.layer.addFeatures(add_features) + if len(delete_features) > 0: + self.layer.deleteFeatures(delete_features) + + self.layer.commitChanges() + + + def load_raster_file(self): + ds = gdal.Open(self.path) + if ds is None: + return + self.layer = QgsRasterLayer(self.path, self.name) + + @staticmethod + def from_dict(data, root = None): + result = ResultLayer(data['name'], data['layer_type']) + result.wkt = data['wkt'] + if root is not None: + result.load_file(str(Path(root) / data['path'])) + else: + result.load_file(data['path']) + return result + + def to_dict(self, root=None): + return { + 'name': self.name, + 'layer_type': self.layer_type, + 'wkt': self.wkt, + 'path': self.path if root is None else str(Path(self.path).relative_to(root)) + } + # def load_file(self, path): + class PairLayer: @@ -219,14 +365,19 @@ class PairLayer: 'l1_name': self.l1_name, 'l2_name': self.l2_name, 'cell_size': self.cell_size, + 'results': [r.to_dict(root) for r in self.results], + 'name': self.name + } else: return { 'pth1': relative_path(self.pth1, root), 'pth2': relative_path(self.pth2, root), + 'name': self.name, 'l1_name': self.l1_name, 'l2_name': self.l2_name, 'cell_size': self.cell_size, + 'results': [r.to_dict(root) for r in self.results] } @staticmethod @@ -237,23 +388,37 @@ class PairLayer: layer = PairLayer(os.path.join(root, data['pth1']), os.path.join(root, data['pth2']), data['cell_size']) layer.l1_name = data['l1_name'] layer.l2_name = data['l2_name'] + layer.name = data['name'] + + for r in data['results']: + layer.results.append(ResultLayer.from_dict(r, root)) # layer.grid_layer = GridLayer.from_dict(data['grid_layer']) return layer def __init__(self, pth1, pth2, cell_size) -> None: self.pth1 = pth1 self.pth2 = pth2 + self.enable = True + self.l1_enable = True + self.l2_enable = True + self.grid_enable = True self.id = str(uuid.uuid1()) - + self.name = '{}-{}'.format(os.path.basename(pth1), os.path.basename(pth2)) self.l1_name = os.path.basename(pth1) self.l2_name = os.path.basename(pth2) self.cell_size = cell_size - - # self.grid_layer = GridLayer(cell_size) - self.msg = '' self.checked = False - + + self.xsize = 0 + self.ysize = 0 + self.xres = 0 + self.yres = 0 + + self.wkt = None + + self.results:List[ResultLayer] = [] + def check(self): if self.checked: return self.checked @@ -274,6 +439,14 @@ class PairLayer: self.msg = '图层尺寸不一致' return False + self.xsize = ds1.RasterXSize + self.ysize = ds1.RasterYSize + + self.xres = ds1.GetGeoTransform()[1] + self.yres = ds1.GetGeoTransform()[5] + + self.wkt = ds1.GetProjection() + self.grid_layer = GridLayer(self.cell_size, ds1) del ds1