Skip to content

Commit a26ea52

Browse files
committed
Verision 2.0.3
Added option to create project by copying current project
1 parent 174e4d1 commit a26ea52

4 files changed

Lines changed: 206 additions & 51 deletions

File tree

QSWAT3/QSWAT/Makefile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ ifeq ($(QSWAT_PROJECT), QSWAT)
3434
PYD_FILES9 = $(PYX_FILES:.pyx=.cp39-win_amd64.pyd)
3535
PYD_FILES = $(PYD_FILES12) $(PYD_FILES9)
3636
EXTRAPACKAGES = imageio
37+
PYTHON = $(OSGEO4W_ROOT)\apps\Python312\python
38+
PYTHON39 = $(OSGEO4W_39_ROOT)\apps\Python39\python
3739
else ifeq ($(QSWAT_PROJECT), QSWAT3)
3840
COMPILER = msvc
3941
PYD_FILES = $(PYX_FILES:.pyx=.cp37-win32.pyd)
@@ -42,10 +44,12 @@ else ifeq ($(QSWAT_PROJECT), QSWAT3_9)
4244
COMPILER = msvc
4345
PYD_FILES = $(PYX_FILES:.pyx=.cp39-win_amd64.pyd)
4446
EXTRAPACKAGES = imageio
47+
PYTHON39 = $(OSGEO4W_39_ROOT)\apps\Python39\python
4548
else ifeq ($(QSWAT_PROJECT), QSWAT3_12)
4649
COMPILER = msvc
4750
PYD_FILES = $(PYX_FILES:.pyx=.cp312-win_amd64.pyd)
4851
EXTRAPACKAGES = imageio
52+
PYTHON = $(OSGEO4W_ROOT)\apps\Python312\python
4953
else ifeq ($(QSWAT_PROJECT), QSWAT3_64)
5054
COMPILER = msvc
5155
PYD_FILES = $(PYX_FILES:.pyx=.cp37-win_amd64.pyd)
@@ -101,10 +105,10 @@ compile: $(RESOURCE_FILES) $(PYD_FILES)
101105
python setuppyx.py build_ext --inplace --compiler=$(COMPILER)
102106

103107
%.cp39-win_amd64.pyd : %.pyx
104-
$(OSGEO4W_39_ROOT)\apps\Python39\python setuppyx3_9.py build_ext --inplace --compiler=$(COMPILER)
108+
$(PYTHON39) setuppyx3_9.py build_ext --inplace --compiler=$(COMPILER)
105109

106110
%.cp312-win_amd64.pyd : %.pyx
107-
$(OSGEO4W_ROOT)\apps\Python312\python setuppyx3_12.py build_ext --inplace --compiler=$(COMPILER)
111+
$(PYTHON) setuppyx3_12.py build_ext --inplace --compiler=$(COMPILER)
108112

109113
#%_rc.py : %.qrc
110114
# pyrcc5 -o $*_rc.py $<

QSWAT3/QSWAT/QSWATTopology.py

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -805,13 +805,15 @@ def trySubbasinAsSWATBasin(self, wshedLayer: QgsVectorLayer, polyIndex: int, sub
805805
mmin = numShapes
806806
mmax = 0
807807
ignoreCount = 0
808+
result = True
808809
request = QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry).setSubsetOfAttributes([subIndex, polyIndex])
809810
for polygon in wshedLayer.getFeatures(request):
810811
attrs = polygon.attributes()
811812
nxt = attrs[subIndex]
812813
basin = attrs[polyIndex]
813814
if basin not in self.basinToLink:
814-
return False
815+
result = False
816+
break
815817
link = self.basinToLink[basin]
816818
if link not in self.upstreamFromInlets and basin not in self.emptyBasins:
817819
if ((nxt > 0) and basin not in self.basinToSWATBasin and nxt not in self.SWATBasinToBasin):
@@ -820,14 +822,60 @@ def trySubbasinAsSWATBasin(self, wshedLayer: QgsVectorLayer, polyIndex: int, sub
820822
self.basinToSWATBasin[basin] = nxt
821823
self.SWATBasinToBasin[nxt] = basin
822824
else:
823-
return False
825+
result = False
826+
break
824827
elif nxt == 0:
825828
# can be ignored
826829
ignoreCount += 1
827830
else:
828-
return False
831+
result = False
832+
break
829833
expectedCount = numShapes - ignoreCount
830-
return (mmin == 1) and (mmax == expectedCount) and (len(self.basinToSWATBasin) == expectedCount)
834+
if result and (mmin == 1) and (mmax == expectedCount) and (len(self.basinToSWATBasin) == expectedCount):
835+
return True
836+
# we failed with wshedLayer but for HAWQS with existing Watershed table we can try using it by matching subbasins by area
837+
if not self.isHAWQS:
838+
return False
839+
if not self.db.hasData('Watershed'):
840+
return False
841+
areaIndex = self.getIndex(wshedLayer, QSWATTopology._AREA, ignoreMissing=True)
842+
if areaIndex < 0:
843+
return False
844+
# failed attempt may have put data in these, so clear them
845+
self.basinToSWATBasin.clear()
846+
self.SWATBasinToBasin.clear()
847+
mmin = numShapes
848+
mmax = 0
849+
watershedAreas = dict()
850+
with self.db.connect() as conn:
851+
sql = 'SELECT Subbasin, Area FROM Watershed'
852+
for row in conn.execute(sql):
853+
watershedAreas[int(row[0])] = double(row[1])
854+
request = QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry).setSubsetOfAttributes([polyIndex, areaIndex])
855+
for polygon in wshedLayer.getFeatures(request):
856+
attrs = polygon.attributes()
857+
basin = attrs[polyIndex]
858+
area = double(attrs[areaIndex])
859+
if basin not in self.basinToLink:
860+
result = False
861+
break
862+
link = self.basinToLink[basin]
863+
if link not in self.upstreamFromInlets and basin not in self.emptyBasins:
864+
nxt = -1
865+
for (sub, subArea) in watershedAreas.items():
866+
if abs(area - subArea) <= 1:
867+
nxt = sub
868+
break
869+
if ((nxt > 0) and basin not in self.basinToSWATBasin and nxt not in self.SWATBasinToBasin):
870+
if nxt < mmin: mmin = nxt
871+
if nxt > mmax: mmax = nxt
872+
self.basinToSWATBasin[basin] = nxt
873+
self.SWATBasinToBasin[nxt] = basin
874+
else:
875+
return False
876+
else:
877+
return False
878+
return (mmin == 1) and (mmax == numShapes) and (len(self.basinToSWATBasin) == numShapes)
831879

832880
@staticmethod
833881
def snapPointToReach(streamLayer: QgsVectorLayer, point: QgsPointXY, threshold: float, isBatch: bool) -> Optional[QgsPointXY]:
@@ -1084,7 +1132,8 @@ def makeRivsShapefile(self, gv):
10841132
ft = FileTypes._STREAMS
10851133
streamTreeLayer = QSWATUtils.getLayerByLegend(FileTypes.legend(ft), root.findLayers())
10861134
if streamTreeLayer is None:
1087-
QSWATUtils('Cannot find {0} layer'.format(FileTypes.legend(ft)))
1135+
QSWATUtils.error('Cannot find {0} layer'.format(FileTypes.legend(ft)), self.isBatch)
1136+
return
10881137
streamLayer = streamTreeLayer.layer()
10891138
streamFile = QSWATUtils.layerFilename(streamLayer)
10901139
QSWATUtils.copyShapefile(streamFile, Parameters._RIVS, gv.tablesOutDir)
@@ -1116,7 +1165,8 @@ def makeSubsShapefile(self, gv):
11161165
ft = FileTypes._WATERSHED
11171166
wshedTreeLayer = QSWATUtils.getLayerByLegend(FileTypes.legend(ft), root.findLayers())
11181167
if wshedTreeLayer is None:
1119-
QSWATUtils('Cannot find {0} layer'.format(FileTypes.legend(ft)))
1168+
QSWATUtils.error('Cannot find {0} layer'.format(FileTypes.legend(ft)), self.isBatch)
1169+
return
11201170
wshedLayer = wshedTreeLayer.layer()
11211171
wshedFile = QSWATUtils.layerFilename(wshedLayer)
11221172
QSWATUtils.copyShapefile(wshedFile, Parameters._SUBS, gv.tablesOutDir)

QSWAT3/QSWAT/QSWATUtils.py

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1525,31 +1525,33 @@ def colourHAWQSLanduses(treeLayer: QgsLayerTreeLayer, gv: Any) -> None:
15251525
ListFuns.insertIntoSortedList(val, gv.db.landuseVals, True)
15261526
# remove unique values table layer
15271527
proj.removeMapLayer(outTreeLayer.layerId())
1528-
# # make landuseCodes table from lookup table: no longer used
1529-
# gv.db.landuseCodes.clear()
1530-
# title = proj.title()
1531-
# table, found = proj.readEntry(gv.attTitle, 'landuse/table', '')
1532-
# if not found:
1533-
# QSWATUtils.loginfo('Cannot find landuse lookup table in project file')
1534-
# sql = 'SELECT LANDUSE_ID, SWAT_CODE FROM {0}'.format(table)
1535-
# with gv.db.connect(readonly=True) as conn:
1536-
# for row in conn.execute(sql):
1537-
# gv.db.landuseCodes[int(row[0])] = row[1]
1538-
# FileTypes.colourLanduses(layer, gv)
1539-
# make landuse code and colour table for NLCD landuses
1540-
items: List[QgsPalettedRasterRenderer.Class] = []
1541-
sql = 'SELECT SWAT_CODE, Name, Red, Green, Blue FROM NLCD_CDL_color_scheme WHERE LANDUSE_ID=?'
1542-
for i in gv.db.landuseVals:
1543-
row = gv.db.connRef.execute(sql, (i,)).fetchone()
1544-
if row is None:
1545-
QSWATUtils.error('Unknown NLCD_CDL landuse value {0}'.format(i), gv.isBatch)
1546-
return
1547-
label = '{0} ({1})'.format(row[1], row[0])
1548-
item = QgsPalettedRasterRenderer.Class(i, QColor(int(row[2]), int(row[3]), int(row[4])), label)
1549-
items.append(item)
1550-
renderer = QgsPalettedRasterRenderer(layer.dataProvider(), 1, items)
1551-
layer.setRenderer(renderer)
1552-
layer.triggerRepaint()
1528+
# # make landuseCodes table from lookup table if NLCD_CDL_color_scheme not available
1529+
if not gv.db.connTableExists('NLCD_CDL_color_scheme', gv.db.connRef):
1530+
gv.db.landuseCodes.clear()
1531+
title = proj.title()
1532+
table, found = proj.readEntry(gv.attTitle, 'landuse/table', '')
1533+
if not found:
1534+
QSWATUtils.loginfo('Cannot find landuse lookup table in project file')
1535+
sql = 'SELECT LANDUSE_ID, SWAT_CODE FROM {0}'.format(table)
1536+
with gv.db.connect(readonly=True) as conn:
1537+
for row in conn.execute(sql):
1538+
gv.db.landuseCodes[int(row[0])] = row[1]
1539+
FileTypes.colourLanduses(layer, gv)
1540+
else:
1541+
# make landuse code and colour table for NLCD landuses
1542+
items: List[QgsPalettedRasterRenderer.Class] = []
1543+
sql = 'SELECT SWAT_CODE, Name, Red, Green, Blue FROM NLCD_CDL_color_scheme WHERE LANDUSE_ID=?'
1544+
for i in gv.db.landuseVals:
1545+
row = gv.db.connRef.execute(sql, (i,)).fetchone()
1546+
if row is None:
1547+
QSWATUtils.error('Unknown NLCD_CDL landuse value {0}'.format(i), gv.isBatch)
1548+
return
1549+
label = '{0} ({1})'.format(row[1], row[0])
1550+
item = QgsPalettedRasterRenderer.Class(i, QColor(int(row[2]), int(row[3]), int(row[4])), label)
1551+
items.append(item)
1552+
renderer = QgsPalettedRasterRenderer(layer.dataProvider(), 1, items)
1553+
layer.setRenderer(renderer)
1554+
layer.triggerRepaint()
15531555

15541556
@staticmethod
15551557
def colourSoils(layer: QgsRasterLayer, gv: Any) -> None:

QSWAT3/QSWAT/qswat.py

Lines changed: 117 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@
3636

3737
from qgis.PyQt.QtCore import QObject, QSettings, Qt, QTranslator, QFileInfo, QCoreApplication, qVersion
3838
from qgis.PyQt.QtGui import QIcon
39-
from qgis.PyQt.QtWidgets import QAction
40-
from qgis.core import Qgis, QgsProject, QgsRasterLayer, QgsVectorLayer, QgsUnitTypes, QgsApplication
39+
from qgis.PyQt.QtWidgets import QAction, QMessageBox, QFileDialog, QInputDialog
40+
from qgis.core import Qgis, QgsProject, QgsRasterLayer, QgsVectorLayer, QgsApplication
4141
from qgis.analysis import QgsNativeAlgorithms
4242
from processing.core.Processing import Processing # type: ignore # @UnusedImport
4343

@@ -46,6 +46,8 @@
4646
import time
4747
import sys
4848
import traceback
49+
from zipfile import ZipFile
50+
import shutil
4951
from typing import Dict, List, Set, Tuple, Optional, Union, Any, TYPE_CHECKING, cast # @UnusedImport
5052

5153
# Import the code for the dialog
@@ -77,7 +79,7 @@ class QSwat(QObject):
7779
"""QGIS plugin to prepare geographic data for SWAT Editor."""
7880
_SWATEDITORVERSION = Parameters._SWATEDITORVERSION
7981

80-
__version__ = '2.0.2'
82+
__version__ = '2.0.3'
8183

8284
def __init__(self, iface: Any) -> None:
8385
"""Constructor."""
@@ -230,27 +232,114 @@ def about(self) -> None:
230232

231233
def newProject(self) -> None:
232234
"""Call QGIS actions to create and name a new project."""
233-
self._iface.actionNewProject().trigger()
234-
# save the project to force user to supply a name and location
235-
self._iface.actionSaveProjectAs().trigger()
236-
self.initButtons()
237-
# allow time for project to be created
238-
time.sleep(2)
239235
proj = QgsProject.instance()
240-
projFile = proj.fileName()
241-
if not projFile or projFile == '':
242-
# QSWATUtils.error('No project created', False)
243-
return
244-
if not QFileInfo(projFile).baseName()[0].isalpha():
245-
QSWATUtils.error('Project name must start with a letter', False)
246-
if os.path.exists(projFile):
247-
os.remove(projFile)
248-
return
236+
madeCopy = False
237+
if os.path.isfile(proj.fileName()):
238+
query = QSWATUtils.question('Do you want to create a new copy of the current project?', False, False)
239+
if query == QMessageBox.Yes:
240+
self.copyProject(proj)
241+
proj = QgsProject.instance()
242+
madeCopy = True
243+
if not madeCopy:
244+
self._iface.actionNewProject().trigger()
245+
# save the project to force user to supply a name and location
246+
self._iface.actionSaveProjectAs().trigger()
247+
self.initButtons()
248+
# allow time for project to be created
249+
time.sleep(2)
250+
proj = QgsProject.instance()
251+
projFile = proj.fileName()
252+
if not projFile or projFile == '':
253+
# QSWATUtils.error('No project created', False)
254+
return
255+
if not QFileInfo(projFile).baseName()[0].isalpha():
256+
QSWATUtils.error('Project name must start with a letter', False)
257+
if os.path.exists(projFile):
258+
os.remove(projFile)
259+
return
249260
self._odlg.raise_()
250261
self.setupProject(proj, False)
251262
assert self._gv is not None
252263
self._gv.writeMasterProgress(0, 0)
253264

265+
def copyProject(self, proj: QgsProject):
266+
title = 'Select directory for copied project. This is the directory where the .qgs file will be stored.'
267+
projDir, qgsorzFile = os.path.split(proj.fileName())
268+
projDir = os.path.abspath(projDir)
269+
projName = os.path.splitext(qgsorzFile)[0]
270+
projPath = os.path.join(projDir, projName)
271+
while True:
272+
try:
273+
newProjDir = QFileDialog.getExistingDirectory(None, title, projDir)
274+
if not newProjDir:
275+
return ''
276+
newProjDir = os.path.abspath(newProjDir)
277+
newProjName, ok = QInputDialog.getText(None, u'QSWAT project name', u'Please enter the new project name, starting with a letter:')
278+
if not ok:
279+
return ''
280+
if not str(newProjName[0]).isalpha():
281+
QSWATUtils.error(u'Project name must start with a letter', False)
282+
continue
283+
newProjPath = os.path.join(newProjDir, newProjName)
284+
if projPath == newProjPath:
285+
QSWATUtils.error('You are trying to copy a project to itself. Please choose a different directory or a different project name.', False)
286+
continue
287+
elif os.path.samefile(projDir, newProjDir):
288+
# same directory but different project names: no problem
289+
break
290+
elif newProjDir.startswith(projDir):
291+
QSWATUtils.error('Current project inside new project: please choose another directory', False)
292+
continue
293+
elif projDir.startswith(newProjDir):
294+
QSWATUtils.error('New project inside current project: please choose another directory', False)
295+
continue
296+
else: # ok
297+
break
298+
except:
299+
QSWATUtils.error('Failed to copy project: {0}'.format(traceback.format_exc()), False)
300+
return
301+
if os.path.isdir(newProjPath):
302+
result = QSWATUtils.question('Directory {0} already exists. Do you want to overwrite it?'.format(newProjPath), False, False)
303+
if result == QMessageBox.No:
304+
return
305+
# copy files
306+
shutil.copytree(projPath, newProjPath, dirs_exist_ok=True)
307+
# expand .qgz if necessary
308+
if qgsorzFile.endswith('.qgz'):
309+
with ZipFile(proj.fileName()) as zf:
310+
zf.extract(member=projName + '.qgs', path=newProjDir)
311+
qgsFile = os.path.join(newProjDir, projName + '.qgs')
312+
qgsExtracted = True
313+
else:
314+
qgsFile = proj.fileName()
315+
qgsExtracted = False
316+
sameProjName = projName == newProjName
317+
if not sameProjName: # new and old qgs will be same file if project names the same, and won't need fixing
318+
# make new .qgs by editing old one, changing project name
319+
newQgsFile = os.path.join(newProjDir, newProjName + '.qgs')
320+
with open(qgsFile, 'r') as inqgs, open(newQgsFile, 'w', newline='') as outqgs:
321+
for line in inqgs:
322+
line = line.replace('projectname="{0}"'.format(projName), 'projectname="{0}"'.format(newProjName))
323+
line = line.replace('<title>{0}</title>'.format(projName), '<title>{0}</title>'.format(newProjName))
324+
line = line.replace(' <{0}>'.format(projName), ' <{0}>'.format(newProjName))
325+
line = line.replace(' </{0}>'.format(projName), ' </{0}>'.format(newProjName))
326+
line = line.replace('./{0}/'.format(projName), './{0}/'.format(newProjName))
327+
outqgs.write(line)
328+
# clean up
329+
if qgsExtracted:
330+
os.remove(qgsFile)
331+
# rename project database
332+
mdb2 = os.path.join(newProjPath, projName) + '.mdb'
333+
newMdb = os.path.join(newProjPath, newProjName) + '.mdb'
334+
# Cannot rename to existing file
335+
if os.path.isfile(newMdb):
336+
os.remove(newMdb)
337+
os.rename(mdb2, newMdb)
338+
proj.clear()
339+
proj.read(newQgsFile)
340+
341+
342+
254343
def existingProject(self) -> None:
255344
"""Open an existing QGIS project."""
256345
self._iface.actionOpenProject().trigger()
@@ -385,6 +474,16 @@ def setupProject(self, proj, isBatch, isHUC=False, isHAWQS=False, useSQLite=Fals
385474
QSWATUtils.getLayerByFilename(root.findLayers(), pointSourcesFile, FileTypes._EXTRAPTSRC, self._gv, None, QSWATUtils._WATERSHED_GROUP_NAME)
386475
canvas = self._iface.mapCanvas()
387476
canvas.zoomToFullExtent()
477+
# if running existing watershed and inlets/outlets file is 'points.shp' then make editor available
478+
if self._gv.existingWshed:
479+
useOutlets, found = proj.readBoolEntry(self._gv.attTitle, 'delin/useOutlets', True)
480+
if found and useOutlets:
481+
outletFile, found = proj.readEntry(self._gv.attTitle, 'delin/outlets', '')
482+
if found and outletFile != '':
483+
outletFile = QSWATUtils.join(self._gv.projDir, outletFile)
484+
if os.path.split(outletFile)[1] == 'points.shp':
485+
self._odlg.editLabel.setEnabled(True)
486+
self._odlg.editButton.setEnabled(True)
388487
self._odlg.projPath.setText(self._gv.projDir)
389488
self._odlg.mainBox.setEnabled(True)
390489
self._odlg.setCursor(Qt.ArrowCursor)

0 commit comments

Comments
 (0)