From dc0b173178c0a997a6fc3717fb1323089924bdcb Mon Sep 17 00:00:00 2001 From: Miikka Kallio Date: Mon, 7 Oct 2024 14:28:57 +0300 Subject: [PATCH 1/8] Create new land use plan WIP --- arho_feature_template/LandUsePlanDialog.py | 52 +++++++++ arho_feature_template/SimpleDrawTool.py | 16 +++ .../access_violation_plugin.txt | 58 ++++++++++ arho_feature_template/plugin.py | 84 +++++++++++++- .../resources/icons/city.png | Bin 0 -> 25010 bytes .../resources/icons/folder.png | Bin 0 -> 19141 bytes arho_feature_template/update_plan.py | 105 ++++++++++++++++++ arho_feature_template/utils/misc_utils.py | 3 + 8 files changed, 315 insertions(+), 3 deletions(-) create mode 100644 arho_feature_template/LandUsePlanDialog.py create mode 100644 arho_feature_template/SimpleDrawTool.py create mode 100644 arho_feature_template/access_violation_plugin.txt create mode 100644 arho_feature_template/resources/icons/city.png create mode 100644 arho_feature_template/resources/icons/folder.png create mode 100644 arho_feature_template/update_plan.py create mode 100644 arho_feature_template/utils/misc_utils.py diff --git a/arho_feature_template/LandUsePlanDialog.py b/arho_feature_template/LandUsePlanDialog.py new file mode 100644 index 0000000..e1ac784 --- /dev/null +++ b/arho_feature_template/LandUsePlanDialog.py @@ -0,0 +1,52 @@ +from qgis.PyQt.QtWidgets import QDialog, QVBoxLayout, QLabel, QLineEdit, QPushButton, QMessageBox, QComboBox + +class LandUsePlanDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Define Land Use Plan") + + self.layout = QVBoxLayout() + + # ToDo! The id should be automatically set to next available id!! + self.id_label = QLabel("Plan ID:") + self.id_input = QLineEdit(self) + self.layout.addWidget(self.id_label) + self.layout.addWidget(self.id_input) + + # Input for custom name + self.name_label = QLabel("Plan Name:") + self.name_input = QLineEdit(self) + self.layout.addWidget(self.name_label) + self.layout.addWidget(self.name_input) + + # Plan type selection + self.type_label = QLabel("Plan Type:") + self.type_combo = QComboBox(self) + self.type_combo.addItems(["Asemakaava", "Maakuntakaava", "Yleiskaava"]) + self.layout.addWidget(self.type_label) + self.layout.addWidget(self.type_combo) + + self.submit_button = QPushButton("Submit", self) + self.submit_button.clicked.connect(self.on_submit) + self.layout.addWidget(self.submit_button) + + self.setLayout(self.layout) + + self.plan_id = None + self.plan_name = None + self.plan_type = None + + # Maybe the template should be read here and allow setting values in this dialog? + # So open additional fields in the dialog based on the type of plan selected. + + def on_submit(self): + self.plan_id = self.id_input.text() + self.plan_name = self.name_input.text() + # ToDo! plan_type should define which template is used. + self.plan_type = self.type_combo.currentText() + + if not self.plan_id or not self.plan_name: + QMessageBox.warning(self, "Input Error", "Both ID and Name fields must be filled out.") + return + + self.accept() \ No newline at end of file diff --git a/arho_feature_template/SimpleDrawTool.py b/arho_feature_template/SimpleDrawTool.py new file mode 100644 index 0000000..e3bfddb --- /dev/null +++ b/arho_feature_template/SimpleDrawTool.py @@ -0,0 +1,16 @@ +from qgis.gui import QgsMapToolDigitizeFeature, QgsMapToolCapture +from qgis.core import QgsProject +from qgis.utils import iface + +class SimpleDrawTool(QgsMapToolDigitizeFeature): + def __init__(self, canvas): + # Properly initializing with the required parameters + super().__init__(canvas, None, QgsMapToolCapture.CapturePolygon) + + self.canvas = canvas + self.layer = QgsProject.instance().mapLayersByName("Kaava")[0] + iface.setActiveLayer(self.layer) + + def addFeature(self, f): + self.layer.addFeature(f) + self.layer.triggerRepaint() \ No newline at end of file diff --git a/arho_feature_template/access_violation_plugin.txt b/arho_feature_template/access_violation_plugin.txt new file mode 100644 index 0000000..7c76836 --- /dev/null +++ b/arho_feature_template/access_violation_plugin.txt @@ -0,0 +1,58 @@ +from qgis.gui import QgsDockWidget, QgsMapToolDigitizeFeature, QgsAdvancedDigitizingDockWidget, QgsMapToolCapture + +def start_digitizing(self) -> None: + """Create a new QGIS project, add a layer group, create a new empty vector layer for digitizing, and add an OSM layer.""" + + # 1. Create a new project + project = QgsProject.instance() + project.clear() # Clear the current project (like starting a new one) + + # 2. Create a new group in the layer tree + group_name = "uusi kaava" + root = project.layerTreeRoot() + group = root.addGroup(group_name) + + # 3. Create a new empty vector layer (polygon layer for digitizing land use areas) + layer_name = "New Land Use Plan" + crs = "EPSG:4326" # Define the CRS (EPSG:4326 is WGS84, you can use any appropriate CRS) + new_layer = QgsVectorLayer(f"Polygon?crs={crs}", layer_name, "memory") + + if not new_layer.isValid(): + iface.messageBar().pushMessage("Error", "Failed to create new layer", level=3) + return + + # 4. Add the new layer to the project inside the "uusi kaava" group + QgsProject.instance().addMapLayer(new_layer, False) # Add layer to project but don't display it yet + group.insertLayer(0, new_layer) # Insert layer into the group at the first position + + # 5. Set the newly created layer as the active layer so digitizing can start + iface.setActiveLayer(new_layer) + + # 6. Start editing the layer (so the user can begin digitizing) + new_layer.startEditing() + + # 7. Add OpenStreetMap layer + osm_url = "type=xyz&url=https://tile.openstreetmap.org/{z}/{x}/{y}.png" + osm_layer = QgsRasterLayer(osm_url, "OpenStreetMap", "wms") + + if not osm_layer.isValid(): + iface.messageBar().pushMessage("Error", "Failed to add OpenStreetMap layer", level=3) + return + + # Add the OSM layer to the project + QgsProject.instance().addMapLayer(osm_layer) + + # 8. Create an instance of the advanced digitizing dock widget + advanced_digitizing_dock_widget = QgsAdvancedDigitizingDockWidget(iface.mapCanvas(), None) + + # 9. Create the digitizing tool + digitizing_tool = QgsMapToolDigitizeFeature(iface.mapCanvas(), advanced_digitizing_dock_widget, QgsMapToolCapture.CapturePolygon) + + # Ensure the layer is set correctly + digitizing_tool.setLayer(new_layer) # Set the layer to digitize + + # Set the map tool + iface.mapCanvas().setMapTool(digitizing_tool) + + # Notify the user that digitizing is ready + iface.messageBar().pushMessage("Info", f"New project created. Layer '{layer_name}' is ready for digitizing, and OpenStreetMap layer has been added.", level=0) \ No newline at end of file diff --git a/arho_feature_template/plugin.py b/arho_feature_template/plugin.py index f06d83c..99d06f6 100644 --- a/arho_feature_template/plugin.py +++ b/arho_feature_template/plugin.py @@ -1,10 +1,13 @@ from __future__ import annotations +import os + from typing import TYPE_CHECKING, Callable, cast -from qgis.PyQt.QtCore import QCoreApplication, Qt, QTranslator +from qgis.core import QgsProject +from qgis.PyQt.QtCore import QCoreApplication, QTranslator, Qt from qgis.PyQt.QtGui import QIcon -from qgis.PyQt.QtWidgets import QAction, QWidget +from qgis.PyQt.QtWidgets import QAction, QWidget, QDialog from qgis.utils import iface from arho_feature_template.core.feature_template_library import FeatureTemplater, TemplateGeometryDigitizeMapTool @@ -12,6 +15,10 @@ from arho_feature_template.qgis_plugin_tools.tools.i18n import setup_translation from arho_feature_template.qgis_plugin_tools.tools.resources import plugin_name +from arho_feature_template.utils.misc_utils import PLUGIN_PATH +from arho_feature_template.update_plan import update_selected_plan, _check_vector_layer, LandUsePlan +from arho_feature_template.LandUsePlanDialog import LandUsePlanDialog + if TYPE_CHECKING: from qgis.gui import QgisInterface, QgsMapTool @@ -25,6 +32,7 @@ class Plugin: def __init__(self) -> None: setup_logger(Plugin.name) + self.digitizing_tool = None # initialize locale locale, file_path = setup_translation() @@ -121,11 +129,17 @@ def add_action( def initGui(self) -> None: # noqa N802 self.templater = FeatureTemplater() + new_path = os.path.join(PLUGIN_PATH, "resources/icons/city.png") # A placeholder icon + # Land use icons created by Fusion5085 - Flaticon + load_path = os.path.join(PLUGIN_PATH, "resources/icons/folder.png") + # Open icons created by Smashicons - Flaticon + iface.addDockWidget(Qt.RightDockWidgetArea, self.templater.template_dock) self.templater.template_dock.visibilityChanged.connect(self.dock_visibility_changed) iface.mapCanvas().mapToolSet.connect(self.templater.digitize_map_tool.deactivate) + # Add main plugin action to the toolbar self.template_dock_action = self.add_action( "", "Feature Templates", @@ -136,10 +150,74 @@ def initGui(self) -> None: # noqa N802 add_to_toolbar=True, ) + # Add additional action for "Lisää kaava" + new_action = self.add_action( + new_path, + text="Lisää kaava", + triggered_callback=self.start_digitizing, + parent=iface.mainWindow(), # Use iface instead of self.iface + add_to_toolbar=True, + ) + + # Add additional action for "Avaa kaava" + load_action = self.add_action( + load_path, + text="Avaa kaava", + triggered_callback=self.open_plan, + parent=iface.mainWindow(), # Use iface instead of self.iface + add_to_toolbar=True, + ) + def on_map_tool_changed(self, new_tool: QgsMapTool, old_tool: QgsMapTool) -> None: # noqa: ARG002 if not isinstance(new_tool, TemplateGeometryDigitizeMapTool): self.template_dock_action.setChecked(False) + def start_digitizing(self) -> None: + dialog = LandUsePlanDialog() + if dialog.exec_() != QDialog.Accepted: + return + + # Create the LandUsePlan object from user input + new_plan = LandUsePlan(id=dialog.plan_id, name=dialog.plan_name) + plan_type = dialog.plan_type + + update_selected_plan(new_plan) + + layer_name = "Kaava" + + layers = QgsProject.instance().mapLayersByName(layer_name) + kaava_layer = layers[0] + + if not kaava_layer: + iface.messageBar().pushMessage("Error", f"Layer '{layer_name}' not found in the project", level=3) + return + + # new_layer = layers[0] + + if not _check_vector_layer(kaava_layer): + iface.messageBar().pushMessage("Error", f"Layer '{layer_name}' is not a valid vector layer", level=3) + return + + iface.setActiveLayer(kaava_layer) + if not kaava_layer.isEditable(): + kaava_layer.startEditing() + + # digitizing_tool = SimpleDrawTool(iface.mapCanvas()) + + # Set the custom draw tool as the active tool on the canvas + # iface.mapCanvas().setMapTool(digitizing_tool) + + # Notify the user + iface.messageBar().pushMessage( + "Info", + f"Layer '{layer_name}' is filtered by plan '{new_plan.name}' of type '{plan_type}' and ready for digitizing.", + level=0, + ) + + def open_plan(self) -> None: + """Open existing land use plan.""" + pass + def unload(self) -> None: """Removes the plugin menu item and icon from QGIS GUI.""" for action in self.actions: @@ -153,4 +231,4 @@ def dock_visibility_changed(self, visible: bool) -> None: # noqa: FBT001 self.template_dock_action.setChecked(visible) def toggle_template_dock(self, show: bool) -> None: # noqa: FBT001 - self.templater.template_dock.setUserVisible(show) + self.templater.template_dock.setUserVisible(show) \ No newline at end of file diff --git a/arho_feature_template/resources/icons/city.png b/arho_feature_template/resources/icons/city.png new file mode 100644 index 0000000000000000000000000000000000000000..ed82aaf6f5506ed7f2a30b29ea1266015f5189a4 GIT binary patch literal 25010 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 literal 0 HcmV?d00001 diff --git a/arho_feature_template/resources/icons/folder.png b/arho_feature_template/resources/icons/folder.png new file mode 100644 index 0000000000000000000000000000000000000000..9eb2915ff672c769e4dadec99bd63330629f4f28 GIT binary patch literal 19141 zcmYIw1z42b^Y*g~NGZ9btdvTFSb(Gg5-N=-5+W#|ASn&IfJlgh5{k4)3P^{fbayV@ zwRFes|E%xtyFRZ=k+aX9b7s!WJ@?F+-9QaBr3)0S6c7YmP*GOUf*?5fFB~E}2Y&3j z^dEyC#18jVbjZLT4>Gd=@H@G!@>2&0qN*qSg+&TbT?B(Hj*3qlwQXKGI-A(Pgq)q7 zg)QH{aWFNpeJN~XZ~lE=zK=fbs#zvhPN z5v7lA=t`rVst)9inF~vNL!fTW}53VFzd{Mn*kRX1wZ*+x=;s6={6AEpKD#unb}Ac9-eq9 zsnK6K9e*Xm+OH8MaQuX~1_M3(&6dIBW4)QDq7Z+eD<=PYge+d*xcq6uZ1MaZy#b4<-#;}yq=(JXsjR7{D;=E3p+9E`N+$UqXb27s9Q*paD9Y% zk|tvqcoikl7igsTWa4>)@F`o@)qkO8mEwz$!OaY&!jQh+BgsoO15gN=FqMP?=dmQV z&C~Am)cl#Xf8h700@0PkHH!?#nwOW?SsH}NF2UwFVV4-IH@in~$ZY+>K2cD)nF>$b z!&!dhJd{9N)wniGl=tk+2^I0YJxjx0wsUL``pB~zh*yEhT$tj(xbn+8Wv$4=J)aCn z89Y3q-lBtR9^Kc4mZX)7(k@2x?aDK>%D;W>-R#b~aC_WDQrQ6M)wN;2TvgQoKe|3s zr2hO@4D?heISYo`E8Hzs!bzc2YMpqK#reu03%7JT16`3E#(@HUmVj7Y^pg3-g|{a~ zSPna<&6u66OvyKlSu<@=>PRPjvM_}5dmc&qi*L^T(y+^dImX(bD=7an1v=HAwdvDp z4+a)%FfBEAo5aknPUR63hKkOaG*_bfwSoH{59hY-a01VBdDHkPRJ+m12BH^h7A= zkHTC|5w+EkWR&0**g?MKFkFtIx2k-rq+&sxbCtf~+3KRTA<%;so9AkSKZAC|4&=%X znM$e?uMs=`OdpFqaJC49tLXiG?!oCEp0nKCPmF;EL<-1pw?QrzI7A<7F z-jQkZ^jFx<)B+AY%{N%r+;M;JxF}7?4dQw&NvC<`ygsy6YHt7NYMdK_ZCk#WuWIO; z&FYYKlX5ak$(d9B7KJRdNxM0Ml<$5-VUSiOJn3>=hB6VAQ@Or`-_6cqMZuS%|5C}{ zBr_)2Y3dqxOTcod+*%F!xR}W(0yB%k3euK5i<7B;`E4m$2F7fD%HK|HDkW^~bwQL7*NWB?V+q2|7b#-r# z`r>F)Zp~jmMT~dAhXU|!H+&%jjo@#F4;D{++*cQ^;zZy^?iXr*MYgGFF0n%oOX9A4 zh~X$LOwTZuE}G*0ZspI(+WwJ5t}UifFDMsP!1BT3a`NHxvx@BK(a!0hZwG>LDV6qt zWD_qWA3S7$jZroDtuhIgf4@k|r;5{se#IP9dV7q%Vos)8yY2eTP^>y|NWU(b|Heke z;TMfFax_22Zsx%d?1}!hLm?h&Ed&{~B(Geu_wWt;T8sUqWG#a++ulLNV)@cn3qg|= zY+OS`e~&okmo=8IoLg92WfXL4bYixThUD5Z8%xM`?-P`iBC~l+&SPCU=|nTsUW#3O z*VuLjt9fQ^t^%>wSf4wr9VPi#b`*1u2kI_&8`a`v#DJ5hSY5$n84H`go#QsDD>e)K z_U^@O3Bw_)_}y!~S)7L>$G0AdZS(mdqHyr%1N|4!?Y}?QFeyhvemInWTK6xwZq$CO zBW21Eq$sE8eV$`LroS<_Ovc;sJ8t6>6r=S#7S2aGd67L#m(X=XaQaQ-9D;8@li|=;m9)|pk|Enu z_R_gO84t869&RV?$!IKpDxgsQNNVY2;*Cjw73Pq|&G{D`I$F&+|cSbi0;z1w&1Ealz$w zecr0cuh!+;T|3bTEB_?3&w~TnljM?Ia0JQ=vaP~ zg&Zl{9j*x9`=N`NDx+gULA>yM36c@*Z_^gN5&W)Oq>$z9Fhd4?JIn>MHr}cYL->wI z`E}W}X)?yb?6)53%^RxjBQ6U{PK+PNcalnbouY_iIFqZf7Eu>&=m|dyzVbl)TR5Cs zC@a;FzeXad;+fI&c$GclRrV{{dr5u1*!L|C8MrrVKj!&PxqYNDk*}!pO|3}#eRsNJ zAj!*~@STTl-4AYXzsfjB>btk`4o$h-Q`$SKvQ z4KC-D(pY#%gf6_OY_lYJmD*>njHUZFtzq@l*cJHMH%^KqE>$*>69Oe~KF=aC| zL!+JYTd!yaH`&?GF5+4_vTKFPd$o*s9_d%siZy(ie}Xh}&E7F0U(0`wI2P#({s!-T zHE)^!5i@kNGjFT0F!vp0=UI{51W{raP}g_rE&O$d`TY3;8~omMah-+~i`<(!7w+Zj zrZQO`ntF24)d;yV%2aA)4fV7Qio`Hp^PY)CahOM_at5Okh#8%(*9XArwR}4O)`zu4Cx@5k|ggGuLaB!M_vhNpyNFY3794>+7J%d_@sL1OjMyI9021IoRb2PG;1@>e+KAa35T)I6%)<@qql+{YdMw?Do6 z&8wV>r3G$1+&&H1kNy)#gNxjewO4vFFV1(r$fULm#V}9;CtJD1%HQ$y5=Cg2L7A1P z7q(Fz-Q|TcSADEBlS&{sK5&wn~&2CUU%{<^3 z)(q4s{*)Hy?ti>3L+p&Fh*b9OlR{kjUS>b?0`s(47))tvZ}7%#G|f6Kk;q!yv}X@~ zB>vfUL*=xesN8X(_2XM{`}y3oVJdkiFl{~klvL-rfK-AJaMc`GE!qxaq+CL~DojVd z75i!&K7qn0GaI7u#YmrXCF_NXlR8uO7zwu9ESV+k1S2tx66pd{O`E{v-fC22zkE0Q z%(^M@Zh(c%7aTMCR`CzjQX8BOD!@?9){=G1N@@Q=$KZHq#k>klz&6o0j9_&R(`N(qT0>byV$MWGx(R zFk8E%Wk}WKM8i6E+{ZNeGc0$s^DcyTJ<>Vuc`6XqS%Q4;(*&M#^l&$w<+1WlOtKco zn#z)FDq$qZplgbXkJ}Agdw8r#lb+{696Wrx?tcc^q{f}7SbYLiLl5rC4UL4{gV5*E z#vQ=Wmb2LPV~UX)ra*%TvvKNPPAE8?NKf?|hm z8MQ}8`EG-)FGcc@?_25hVTpHdej=_FoPbHQUJc77N2e0;LRwx&O;F-86k>`9a|w19 zORj#4U)>D^Z|@!ydg4fqz`v0s#m+MO;a)bzwH+3AFjHnjVRkSdp&I zd3nl_Kv1^8#uNQnQYUh`)u4G~4bja+MZE%wjwolr>h_In5*r)CTUY~d&=%hpPnAk2 zI$_=Bl9Xp3;j@WueeIG_UhPhdtd$yKu$72B5yu!^jv7D3CMNH^;t7)OmY2e#?hlAY zukGhP;$if~n0jBYF6JkB{X!3dYK}VIP~91HB4Zu>(8nmtmAp98d6#rE(Y~iW@xPgnMcmB~4GoaRd6#QnAza}-lmIHiQI)ot7_%_Q*B~0_x+;zpdF7)sD}S-k z_969V+PrTEUA$eqh=Bfvbil>$TvqyStJKhG0+!7Yg3Ud)(h-3>`ok@$TG3MZZ&0QIOwa^90`b`%R*Q@d-i_PC%>un z*Taib@R9Yq`VUSeej;$YTp9~6FP+R~NL75{Nhr){aiU`#15%h1ZHqy=#Rc8Zca!(1 ztzX@8znF6QI zqfE`hM3!6Vq$(@*Bzljf_cLGbF#q7RzVy4#_xu2w7Alf)RE4p2k2BD2mNIYb?K#{Q z!%*Gx{a*B9WKlQ5fsISmyG^X{)~O_;@UQL>I+9v_7?g^(=qtBM!0d+IlM z>|HXV#3c=PWKC4!P+T^)=o_uBA<2{Ux=8_Fz8Ut`dmlyvyFOYSoPD=ZPu#M zzAS+_f{I@qzA~w6dA44Rd{1@rWZ#&5{o)2Y^xE1yDQRio)3@x!-(&$-L*Q3gM~Rs$ zE3vo?4>DEaZ0TA0u6w`hflG%}xIZwSjeJrSt3Dab!3ZhT^%e%5X1*IJbRS)pKIFw% zS{Q#?PE8KFtA%!R&TYHS8yE|b~B&4;y%NJ zy;$XKzWwB-@vvBkZKMQ%=?S2O-;hB8H9RPeogUQHX3E^gC-6Q&t(0X!2CpSaKgn$^ z6KIP(Ms&&ekTTd}*NCclz&b0<+M*0Ky0{x(=TJ;kmJ=|8f?I&7b3gHLTT?*f_%oKd zKhzmU6KNyKF(e-}G-xy8l`BZR;aU8A0of))GGGkAk6TMlw!*8nthxkQ|4tAJ@F}7X zlFjV7wOLs$rep1@6{2Ld$lauhw?dOb6@yL)R?P|K3Ri5#N4-(1nd?9)kpq-dqUmqn ze`SkeWSS_$il#gA^pPk4;A%IaXKX1Ks&5xoAb zZ!07^a_oaEl1Zhb?KLzG4=?Z}*FTM|B9)k)Xk&IgX{S&19C9KX`VDa;*MGZ=bkSSE zTQw#2&>XF33Pe?PIh~Ityrgifu^`j0Qi5f^Nx@~`63pWnv4yDHJry^n`k1ti%DnvYairB>UvtmRo zc02E^)HmjiQiVGLee(89#CtLCjZxA_!advpN~aBXhMWC+!G5Vu(p5>0jl~aVvLO9{ zXN2mr4&jBZiAbD4KJPGCUJU8E?Vi3pLd8%6M+eZ^_&81rs*wg~-3Tq8O$Mx)d>K5z#PV8>6;E+r=i6=+`(7poa@aTe z_a5A>;Yn=z+$^wjA=|hB$*lrp55Ey|9ka!{!2$&Ng)o+WJ>LsLS~!BR4Bv*D>~l%@ zCqq?^>hE@#t^zO@K_~XDY&P7fs^5Voos_=oglNNw2)m7)sa+KEwo5zA{h?=cdL1J= z6G8&fN-Sn)RBuJqxU?arjeX|Zq%n7Ag8z+kSk{qG+{cKfOXW&uR_%Q$mak_a%-)OX zrN)HFy`-C}cYayv*033#kCvGmg>GJY#q;IQs8ZWNfDWLQPOin>^w6n z$l=vw)!Kz((|TIMEQ8OqP`Ef3*o%N=dJ0+na6sm9UOG@Ys!k_xfFnz+*DyZv1= z_P{U=!uYElrkC5gDS(o22o)p~rp4SE*RIdBlvw-!Z;Zb3LVDySj3_SzuUJf%j7Eau z0~=wU#h*U|hTQRmNME1nz}(3~Bo`Gp6M>fd z=YQT?@yP*pjp|3=|GZ~0fMAgdC(~GT zOLC2P^;aV^Nu_>*#G_qrD$}f#5^Gw_jn2Az3$X~zzl2$2b8mP3^|g^aN0OCSIa1N* z6u=93rb1zYUGK-}7$KnP8ig(P;u{JCb56cnadj^kONJ1a_S$d#xG=0n%K%O*;M|t} zP9c^|2PU)0f?-MaAqc<`vUQu2D1<~Uv4p)~W`?|2UV&i6$#=~(YkI~=!xTm;dJaaZ zXh{eYeAonxokaQa05CzC|3+-tDA6fyw-V@@3ENc8z1CG5FdA}~A(4wcKd$yRK}>mY zxD^Uc-~<>r_!rp5P>QOU-#-6m+6wN}vPJ6KmjqPo?2`z=U56co6>nmmCnf75)oeH|jq zM$2a6#L>V*bajNKDf?8u2uou^u(F#z=Yz!i*8uC}SMatQA9X0lvi*BO$LELHA26qi z&kbuOvWtW%=-8I?Am!^YMmb%pMY4aQ(Xm+`Y9zk@?zM}IW;2yk_)1Vig6*h`P|1mEAQF|( zGIHB+KC*t&Daj(^4`lvBw`ae#n}KKT?QgNTN~1B>q~t`D&>fAt9ERtX5Q$B4+oV#fu^oND{1IL*RVycYwABapGhk>O?{LN&!Q5tW!C6@BvM#e zZ8~D2gz2O`=L8w((GUtfw8guD_q(510BGR1`mQJWGBnyXd3HvAW0zAl7jrs>%_pwH z8bsDmp|gVEBhz=|Rpp&73gduyMrN=uM@3N=!9HFJiAU&XchcDRpP%H*FN}2Rk_$p? zm$T0;EF)4zeABTv6}Ciwc6$eJKdwn}zwEH|5uf9ucelOLnu*;_cc+(|3c1kt!k+~F zfC63sN-t3>lOyU1KVBq?rw_jZ)-e2IWcdW8OuW4w!IxwG=dinLq*(_nVbJpulv0S| zPQ&RVeNUjz8|>eFH6!kN=}}}7+uuFOuR~SO>V0|hSDjHX=Nwho__nn$Sx zkaES_FSQ?b@5)!p?fRCDm6AS-zGQFak^%B1j_LM&gc*;`J_kkX-3BKNEibY9tJDj4 zc!SmKH{G~4BYOk!K*Dnwh-=TZBoO(Qi&%n+^R}}ZU z@_T2=-)kvGbT_h#Xm+RmCYpL*w4Cv?$}zd7I%|HW&gf~2RAdbUm$z9O>GX`c4(%FL zYGUd}LE&pn%w~{HI+kxfvg<4GjnBg13f=wD3uYzelIJvEJmLfXD95=JiHKKu#M+T| zlQF#94#Q5BnWK}M2#of#(X!1My=FuIv2?R#0+t!!Q}pe#2obcz&Z;vW{U`mN9UMr& zhX_kWe3r1saeY<>@!?EK1R8(RZb^D&x!hf1cobvO=U16lkdb+*+swa2pf0*(S5I3=1m%mT6S*Dufs4~U+P+iWpY!T zhvMpBmYe)OzfSod&|-W&v}?$SgDq` zqwS(>_=Q_uxnO&rvop%=sl)kMJ#A>7`ZU5ha>|K$N2%`4rM_l_I(e&!AZBA(8o3A{ zc>tf~VrWQn6e~6l9DjaYfAW)KdGB*See{aX)~9~JgAmL)Ty`mFN4pjFaG?s~sgPvm zwcXdPG9ie{AyOJ14MFIIGMGId;>dp)J4sr%{H$|$eIOb|6X`n7e)$RE<B?&(irpNReq1+c8+c!EB#Q9|a#{V+5w2uY^(`?8v5 zz)V91(G<`vdkYRy3NlJGbGTYDJ}}t_2{{*@%Oj_O5Pd!p8rPmc_2ld)4-hQCYPr*? zAg_L`8!uc z=8ey7PRTNX6SRF9SguAi9e7T2?l)xnK(qCkFo{dm@sIB@@F03JMu@g)1h3F@8JJCk zP8mtU^LN)&e7@u((I2KMyBpx>nZ=AW+Olfh*?%T0 zmXgMH$6W_C39$wq-zw5G!kbVIlIBMQi ze%qDk>crbppR{StQ>J|UDBB=kAV!rCcWMN{nl`OwWW-|<53NB8QH2-DFn9#LEDMS? zC&%>@mRUiE)h~vA7#6_0j!P>Ai1IeWtBf#w2j)nt_lzxmYiR%jQD?%+o(md>pw8192#^jvo|N@($(>VgKt0L|{D)IqJ_P?pDv zPc|2FP=K%~$nOk9I}FC{Z}~$qRN`7Gbp=Roi+19m+@i4{`tpQgpucmR{KjLpjmfK| zxOO-Pe-O04xt1^^^i#&G{6wD(^QXZH!8Q7k!mD6?SCMJ$OcYo}?X+wJLZV~#)UeG? zIcWquu;SeIVeo_c2L3K1q**(HkKmX+lRj+d%(|90{8Ex4e0DC0&3>cyuOI2dwV6zM z5=C#vf;06Nd)p)ueB~R7%u^X~<_3EYC+eQ!pKR9N6|e)#aA4VR@-4f)RaZ}cADweq z!GzqKi+QzGc_ZF+HW@2li6)UlID*Z7e)IO=zMy59pRZ5A-#(tq4Fl&!rJ)N4Ajd3b z@C!9dte5<;Dk}K)&m_<>Q#!={)cLsVAb9s)j#Tkh3u-Pv5qn^2vM66{N^>RVURA~0 z+L^Wou0oVhoA^nB!FEHGq@C)4J~;2suSY(ReDu;<`B-*541hwS(KmEst%_D;W86%V zLbG>l@T=wVoP~^cR_G&ZEBqICd;OZDKRLD2N8`mmXs$HWEkDGKqvESmuEnT-9@OnC zy){JVDsy4_aB`c>h}Sf7-m2Y7?A>8=3bo0fi~^(|Z)f}?rfnXXtyFdEi&&m~==V$uOFi_O*GH>raad4pdFi@Nhh{WUu}c>zm@E zD*y9~)Zx-(ZU5mW52H7~FmaBA(4M%Hg|yd%a}1VO!EUo?FHNMXaLjk}X8#9Jy!BD; ziM>ZHP$OGf(=cfQ67|{2Wm2mqj-W!Z)cE1O=#?wp!2@Qn=iau*#SyE#j10(;H8D(Vf^$&tn1*0J%s&QoRK3lj7iatuW zH0rPeyjHHTa-`uA@mb04r^T}gofwr~Z_51WB(-vDCC+>eN24-w8kcXMw8Dh4KRkQETgwix;q67T)d_8P8Awd>Tt59H~#e3tPp?Z=@^ znq<#!?4&&>M;Nz^1xrkv1G4K>${Z_Cq;d!cNvrXpKEcxrUGBC2p8eCDNxs+;dP!$ zw9K08>P=~kSObTDdQ3Sw9~tMt2QXrw&o~e7kJSGrxJ<8{D~UzL%@ypaCt1{7GI;Wi z<*I5LFn-hhhIXuYE#}E`$yP99btRtkDWvmN8;dWzx9YmDKK)I&LtNHbA`?{0DSFa9 zZ+eP!s>w39y7SF-WM`!!s}Gvk^y38W($8|~JvtB5x{=l~o4#zMB$^emQEqG>ZydZo zisI_FSwDk0KjyjUhJ1~8r^sFud7f_?QUaT7J!iXdQid%c}2o2g}t_e;Pp?7@E2)Qvoo^! zEE3dARVgqQJ5z08#X5nC#m(%s3q1y>u=Ewt^2)@w2 zP#q9^mUcCcBs_Th{H(g4%Pr0-Q`BmLs`K%oV3oZsP;!Lrg<)R+W-lrnM?$U(l2AFA z7f}Q{mfB(2p~68g7{})idOQ3WQI4ymx%-hxl?g4##tSIZ&3F~v!>I^6K>zQ()T~i= zh@PeCvyyCWeorFz+l{zWDngJxd>=J%KA>eZ1O`P`?>tX9h>_m@XrW;oW8^EQ-aHa= zm)ulgYkaIoyz^sYC5~Gw!yHD#VYxVh8h+?jfh809tmE?Z@;zhcF*5W9o4!B&8?4do zWV0iWW2cJRlQb;zk3^77$n`FFQ~d}q!22s$O!$!7-3{y=Ldx^OI#DXfS9*4gtdSdM z%eP$APvh^_!a1&uuealr?k7Dx!J?qp3o}JIiv~6@I~@y|l0F$5h;B zH+X`|q={tX6F}x+)9Wb7(|B_lKNSyX%2}3S#$Ed2sQZ{DFQN=GR;5nXY`mesSJJN-ez6yK&ioUq znL5p)=^^Qk4B5Fo5~6jRWU#!zUbppY8BDD#h+$ zjAC!OdsP&dQY@s7oy9+A`s+D!5pYL~-dhLh-m*^Gy0Z9(hC&7R9t}>wy#UL6*EMH4CELFM;{D>v zk9|m5Bb!e>b;@F|5SqHRJV8Cp-rx&jf7^oV>gMd2?j}vggG5tX#X6N|lU)y>mtk-H zNAVK#GwNzBDwkwC3`6SLg)y4V$v+;^S4IE1t@2S;XHFP;4%0Xn9e>KyFpgJ<97Ib4 zavXi1lxKdp_^ z6;!6+1&RA&Y`oif_O8gMZzSu&KYo6;km@x*tJ$!wB3KhA^725<-7H@jQ{ z&UuJ?8nY#wsaI!uk;g|lSfZwsaH_r|9e%vY0yS#3MzgsA_`||NR{;u=m0-0z zU6cIk15)93!w>q)c}TzeoPJzhv6Xa6ce-n7r$VpeM~HAfck*LM>XY`XA0Kr`fa=1Q z?fVh$gt;6A#+gl@W z6RRe&zIXQR^uFu1W-yIR(33aL3k~CVSpPVu;;8?-c=HwS!35-*fnEo01GlMlJWLQ$ zo$Fmgfpk{$smzx$dh^svc}U-V`}ZRu8!zbrq0W+zobnlaneJzL zaLm56eEg*tmmR?bTqp>>BUgI`BA3A{=U^AvTLG}_26LUPkKs)z_T8-B>30t7JN z^C%vG2xs7f0zB{|ia5aF=5uQ8Caz*MUvz+k`BiSi!-5W{2<1)B+?0LU8u)W%H7ss% zlbs9PlTM|T3D|xe0S$dto_r1$0qB9Nx1b_=wTaVEE#tLLelgaNBD`h?u!iEBKH z5!h=fsk-LzA6vGNyl)n;r2E5Fni73iISu9^Mge8-!$A8{_#<#NXuN=%ny7o-YOB9u zC)hb=DtSNUD_0K7z^VYl1e;~?Y|}_qxZT3UY@Yki1GcJfl@nX; zX9sEcbZJ-2mmf#xogbHL9oa7SRKw8)_&}~%mkmn~?!{x8@6(m_YxU{{dcQU9pd%`d zY|z{ShRZ2!HIXWD`m;nIsx1PN&AbnoWxkcQIRz`nTn3q>JIia=j5+8<#r%&mHb&^g z!Qwy`)E;<0sn;!<4f)GplQQ~4a*}@I2u{=bH5s94yuR4 zOxFc$RaeL{qFlAUp@1jOynI!F<)zgfU79?T$RCZof9HZh*>3894C+{c_7&hm?eAPZ zfy*q!YE@Vnk|$04mK|B|0|X+?c60AgyAyeyk0sd1Sw&9h0@}To5unb98=OIeiFsl-SO`Mxj#xxY^#p&gI%lpuW$gXGvo&vJ{{5>yb*=4tHpq`g_zO=J?s&H;C zUQmdb2O()RZrChfe!<*cPqxkymo|clYOQ$XA}PGeZAEi0F!2dt!pEz;PR4|DUQEbCsxlt1h^a zURPE?v7Sx#Je1qGob-7tcQzH5TTk+C!d8xs<9aqqX@#>w>&xw83 znX%bPLbE15+>kbK*^TGt;H+#x3E+RuNaPI3F%6kBC8P%eRd|8ap-B5siF3!9MIYkm zQ=na9%_INzkul_)C{mx?cuMEQIBD!meXh{k@WB7OVN$g-fms< z<&=pE&&f4JAkP3UReEVh-3L{~{(%_XaQv<4qL2+{XQgh^cR3Im_@Cy6@=tjM?$;5F zr(Di_uY0t&4pblDZ%|MbbiV_)*ca#8KULmkA50pc0Zq;0A+dBI-G6bC5P+LkEp6oO{ngF$9}#`0iUaDzn}@jvt!pUqgYdC)dV-LPLToDV}TsM3qM13q-OxeZ)g; zTj|a8U&i<#3b@_N1C(l#gcB5CVF6lM8r|V+?Khe*Xc2SxcdZ8^+XB5fQZ|0bp!^`= z4?Zv*k)WPiU+tl`9m|eCT?(Ec8D|((i(Zh z&>{j+P+h>Q2-Hl=D%`ve%5jI2nLmMIm%<$2MsMSr@-si$sjjqy^-0_pC^4X6dXYSom@PtY})dMNrg5TwX@ilef<<%>o>%~_1^Cp1qwka9BA%3RM_+y ziYxi@rhxbf3e{rBiTQH!6RQsDRtgcsZ+PMhvrqkRMDXTirD$VOMtB~sAZhOpK+m7` zp*iFmyg3LaTtSkvEk#LH#$Rn1842rvOE20hf4K=CEHnRV^R*q0F2U2%YW`BT_>Mn! zITvcC?4G?(Dpx>Z20#i-$Y(&m|LkfGMC0#--Zw*x*kZx~Y-o8ZdP3a0djEv|DM@d!JPobpw^JFHA&lEjJpuxA7R8(6!LvLE zYBHAEU$ULmJ2RKHPO3kA#|H$=sAm`9y;J@@mnBZnIgBFyP;B^oSOAGT8AH4M4kv#pHYAU_gM;d zX+Dwv+BQ3BPX_w3;b_8hr8lCS@zeo(D}0r3kUQuBU=5hbO`F=AFq0&v*?~h;Slq#z ze-K+JM*SrWt0cbW>cf%F=tH{{Va|XC zBP4-fqC-93t_xWE4?hzGcdeZ`SYpUWSz67pjHyrpgj!2BO}r@ktm*5g1waOuE5)|gCOY82wiWuKOl_v}&J zw=)=Hh04R7$ub=Xjrr)OJ>qB6iee9s0-u3h6v)OD#5PF?XugI zYMlaV>$4!+)U#ZG;|dW_W)GkX95Mp0OC;-18rf(`=R0(?R|bkdWMkG#kS}=i_@G#c z6H-9lX|H(mN44Ek*mueq{*~E@fZ!I7%2E zH}Zve9xnvwKZE8Qf)gQ7_C;h|p3-8+Bxty8`dMgi*4{(8T zj5o&ZqdrlC5)(lSZ%-?PLPx1IgOr&dTi}TQxtDHPtJ4Q9#S%zE?#_>kA{ysn|LK8m zFB;RSk=dN{JHH7c5xWEFV-Gf999+rFUz%R4Pwb~)5kG^hU}!4;#|rt!pLVi3L~pgyqp3hG{W6kh}UcbyVHbA8Q_mQE?W6Be^O=y3};^!z_tQ8P42!d zO#@sGw(K|;G0dycL2&5>9ccqo$$AULT(I8*+1Cs!&C#j~5eC~sTRd;w2c3d?f(*cc zhJVx%_1G1m)jYL4kHO3JQ$P&PPWX#{j~xhu?MI5U4LN7j3fa&wH=3KLq4l51{(jvX z+wlx+e+XJj zX4X7&=pdIY#)ODI4(EgFlSc4>CIR!g{{n*}MMq#E$cPy?+q^6LA1PAUy6XDn^Qv_k zuhL}-$OKq*zzfBVOn1Y+w-~-w+Q0|U3`mU$JRzvElnOuc{R}SGQw{4TxrDZ>0Cg?i z1-J07aSKU>vR?q$)&Aw%g~iPTg|?IbPBy_6WEVR`w7HSid~?Uc&~+#RAk9{S7&AVg z2P@rPGFB(>6a|E{w4Y;FcCkEh?Q0lE+__DVw-<&G;=v1byS94z7!07fKI2q?&f)J6 z4)H}-Jqj>acY)v3&X>+s`3i#wj{0?VWw{@G(v~c!h@rIl%w~05{&80Uc!9XoZp8;{ z2G;&niUeiu+k_Tz?bCK6~uz>Azp&V;f$rvQ$pPyK$j?M}j%;Q!>8 z_8#NdNx}0XL^`UO}^>u=D3;mb=3Si(9rOW z?kvU6C;c(q6p-b?UpIl1|ANJ5u~T!KX#^z0VHnb0*TD;tJKBcHop%l;_yMplil0yz zRVdIsR}l3;mcr*sdp{jW+H^Ezp$&t8%l2#^2W{d^*Mc3kX4+%GIIMH0jhcDl8 zuOtUzA4a&iW_RSOC@TcTSemdqsG12)ya$xgrqyG8ewi8I;6{459BnoA&i?%)rz zOBuKeeC-WS`v~qU=-f+RS`kF97ve!@4uE80x1ZSaCK+G~vVUP;cDChK@s0OC?F95?;_h`;Y-(Zp@kSKMO0>)&)flxu16+st()`s}etwD|t?fxeRpF z{w6Vo5F|6r>nI(&f2W!;S+FdRMWBEaQL7qcC}1D#}|?gfpL1kg~b zId~`*Qo%2$hNa@2q!!9+=}nyGHG?aL#we10b1Nbf?%k6DG@^5jz-djGhDWHw)ZSM#H zkQF7@mwW!*60$CqAAXF>aR0*%yh5X?fe>NG2&aEah1L$m{R_DZ`@8cyC4?5*c?QT8 zX7KIIIQySO@nhFt;yTqZe94Kw2A+Xa=>qf$-*|wYE3nBMen#AswNC3C z;HwWx3s2bpNwO~mk{ti*F5ip$na=Qif2)zsp9Z;rpheAGV?*@2q+kMB=|n~BEnIsz+gBJ)ROXh}lc`%xEy2U0U1{o}MO_xR z)@?4+i;Pg!q)o`&J6&9=Vw9)44i#c`D~(`N8o^WzW064cfR-ILGBU#N?paj&=ruTTHUn;nIi)L$emk(>&Y9{&1ZxS2V)cMBkf$7 zdN2o-lxF@j+#aa70#prHAD~x6!d51sjA5qq08AdW-cqda3?DuJ8*Fh3EDy@IVfFKJ z$hOrA`}6kwhh?p$##Cf^$@Z>CD=lirmvT=bLIYZ$zHaj?EN?C9Q%E7wV!vZmf&mPe zJY*cC)CHa$&;3WHu^&VOW)$+zrI@^2D_O<~SuG!++EY11gy)MPE8J4l7;GKxG&QX+ z%ebVVQ_ieDln(;%fCT)^8mVSK!9qyTw+e~aMNr%g3^5!e?D1?qP=4uRTXd~x5`_4# z+D2(;p03aCP&_E(|F7s5B)OF!(=o$HST2kA4z;3x#Uw_Fbi^so5HxZZ@v0PaPSEo! zp}Nx8oB6$;%+1OFenM-}v<25Qor6eC`T%`8;E4N5f!8*@3vJaD3s8aL>1w1-CEmCh z(l@7HLd1Fps0Y&I2hFUVO}VH_UBJHS+?G~Jea+e&zE zk4pyVi3w6g=G`!~`E!3$y0bFs_!sG@CxKhtMAOP9HLLe#ZCvBi)M(K~Rm`QLo(_I- z$C^m9D0zCYeR{hr#I*ui?6N&nSBNVF*~(y~#5q2`VeetbIJPGOCe14n3L#7Ec>)Lb zg0xV(uA4-^$@cpRdKX)3p#6AwhqayLqxjf50^GDNnCOcqxjWfAOBik zVDb$)E2mdu>eR;7%invl3ze%uFhfjHc$P3MYVVa%2ix*hI-Vt-8*M5XwM=Y^Q1|5U zj1t&to0W!u6Q$Mg+0RVMG+wf1stmn={ePx&Yra2sRk%jvGtmqCt;bOnr@f_b|7;5jZ~>Hh-NsP<(5 literal 0 HcmV?d00001 diff --git a/arho_feature_template/update_plan.py b/arho_feature_template/update_plan.py new file mode 100644 index 0000000..7311453 --- /dev/null +++ b/arho_feature_template/update_plan.py @@ -0,0 +1,105 @@ +from dataclasses import dataclass + +from typing import List + +from qgis.core import QgsProject, QgsVectorLayer, QgsMapLayer + + +# To be extended and moved +@dataclass +class LandUsePlan: + id: str + name: str + + +# To be replaced later +LAYER_PLAN_ID_MAP = { + "Kaava": "id", + "land_use_area": "plan_id", + "Osa-alue": "plan_id", +} + +def update_selected_plan(new_plan: LandUsePlan): + """Update the project layers based on the selected land use plan.""" + id = new_plan.id + for layer_name, field_name in LAYER_PLAN_ID_MAP.items(): + set_filter_for_vector_layer(layer_name, field_name, id) + + +def set_filter_for_vector_layer(layer_name: str, field_name: str, field_value: str): + """Get the layer by name, check validity, and apply a filter based on the field value.""" + # Get the layer(s) by name + layers = QgsProject.instance().mapLayersByName(layer_name) + + # Ensure at least one layer was found + if not _check_layer_count(layers): + return + + # Access the first layer from the list + layer = layers[0] + + # Check if the layer is a valid vector layer + if not _check_vector_layer(layer): + return + + # Apply filtering logic here since the layer is valid + print(f"Layer {layer.name()} is valid for filtering.") + # Further operations like setting filter or using this layer for processing + +def _check_layer_count(layers: list) -> bool: + """Check if any layers are returned.""" + if not layers: + print(f"ERROR: No layers found with the specified name.") + return False + return True + + +def _check_vector_layer(layer: QgsMapLayer) -> bool: + """Check if the given layer is a vector layer.""" + if not isinstance(layer, QgsVectorLayer): + print(f"ERROR: Layer {layer.name()} is not a vector layer: {type(layer)}") + return False + return True + +# def update_selected_plan(new_plan: LandUsePlan): + # id = new_plan.id + # for layer_name, field_name in LAYER_PLAN_ID_MAP.items(): + # set_filter_for_vector_layer(layer_name, field_name, id) + + +# def set_filter_for_vector_layer(layer_name: str, field_name: str, field_value: str): + # Get layer and perform checks + # layers = QgsProject.instance().mapLayersByName(layer_name) + # if not _check_layer_count(layers): + # return + # layer = layers[0] + # if not _check_vector_layer(layer): + # return + + # Check if the layer is empty + # if layer.featureCount() == 0: + # print(f"INFO: Layer '{layer_name}' is empty. No features to filter.") + # return # Simply return without applying any filter + + # else: + # Perform the filtering + # query = f"{field_name} = {field_value}" + # if not layer.setSubsetString(query): + # TODO: Convert to log msg? + # print(f"ERROR: Failed to filter layer {layer_name} with query {query}") + +# def _check_layer_count(layers: List[QgsMapLayer]) -> bool: + # if len(layers) > 1: + # TODO: Convert to log msg? + # print(f"ERROR: Found multiple layers ({len(layers)}) with same name ({layers[0].name()}).") + # return False + # return True + + +# def _check_vector_layer(layer: QgsMapLayer) -> bool: + # if not isinstance(layer, QgsVectorLayer): + # TODO: Convert to log msg? + # print(f"ERROR: Layer {layer.name()} is not a vector layer: f{type(layer)}") + # print(f"Error this is not a vector layer! {layer}") + # return False + # return True \ No newline at end of file diff --git a/arho_feature_template/utils/misc_utils.py b/arho_feature_template/utils/misc_utils.py new file mode 100644 index 0000000..3e5a741 --- /dev/null +++ b/arho_feature_template/utils/misc_utils.py @@ -0,0 +1,3 @@ +import os + +PLUGIN_PATH = os.path.dirname(os.path.dirname(__file__)) \ No newline at end of file From f6ac0cd30bf82588854f43643df733fc62b252be Mon Sep 17 00:00:00 2001 From: Miikka Kallio Date: Wed, 9 Oct 2024 10:55:18 +0300 Subject: [PATCH 2/8] Added "Add new land use plan"-button. - Added "Add new land use plan"-button. - Button opens new_land_use_plan_form -> filters layers -> starts digitizing -> creates feature with digitized geometry and values from the form. --- README.md | 4 + arho_feature_template/LandUsePlanDialog.py | 52 --------- arho_feature_template/SimpleDrawTool.py | 16 --- .../access_violation_plugin.txt | 58 ---------- .../core/create_land_use_plan.py | 65 +++++++++++ arho_feature_template/core/update_plan.py | 53 +++++++++ .../gui/new_land_use_plan_form.py | 37 ++++++ .../gui/new_land_use_plan_form.ui | 107 ++++++++++++++++++ arho_feature_template/plugin.py | 99 ++++++---------- arho_feature_template/update_plan.py | 105 ----------------- arho_feature_template/utils/__init__.py | 0 arho_feature_template/utils/misc_utils.py | 2 +- 12 files changed, 304 insertions(+), 294 deletions(-) delete mode 100644 arho_feature_template/LandUsePlanDialog.py delete mode 100644 arho_feature_template/SimpleDrawTool.py delete mode 100644 arho_feature_template/access_violation_plugin.txt create mode 100644 arho_feature_template/core/create_land_use_plan.py create mode 100644 arho_feature_template/core/update_plan.py create mode 100644 arho_feature_template/gui/new_land_use_plan_form.py create mode 100644 arho_feature_template/gui/new_land_use_plan_form.ui delete mode 100644 arho_feature_template/update_plan.py create mode 100644 arho_feature_template/utils/__init__.py diff --git a/README.md b/README.md index 6224972..75c0077 100644 --- a/README.md +++ b/README.md @@ -61,3 +61,7 @@ the one with the path `.venv\Scripts\python.exe`. This plugin is distributed under the terms of the [GNU General Public License, version 2](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) license. See [LICENSE](LICENSE) for more information. + +### Attributations +Open icons created by Smashicons - Flaticon +Land use icons created by Fusion5085 - Flaticon diff --git a/arho_feature_template/LandUsePlanDialog.py b/arho_feature_template/LandUsePlanDialog.py deleted file mode 100644 index e1ac784..0000000 --- a/arho_feature_template/LandUsePlanDialog.py +++ /dev/null @@ -1,52 +0,0 @@ -from qgis.PyQt.QtWidgets import QDialog, QVBoxLayout, QLabel, QLineEdit, QPushButton, QMessageBox, QComboBox - -class LandUsePlanDialog(QDialog): - def __init__(self, parent=None): - super().__init__(parent) - self.setWindowTitle("Define Land Use Plan") - - self.layout = QVBoxLayout() - - # ToDo! The id should be automatically set to next available id!! - self.id_label = QLabel("Plan ID:") - self.id_input = QLineEdit(self) - self.layout.addWidget(self.id_label) - self.layout.addWidget(self.id_input) - - # Input for custom name - self.name_label = QLabel("Plan Name:") - self.name_input = QLineEdit(self) - self.layout.addWidget(self.name_label) - self.layout.addWidget(self.name_input) - - # Plan type selection - self.type_label = QLabel("Plan Type:") - self.type_combo = QComboBox(self) - self.type_combo.addItems(["Asemakaava", "Maakuntakaava", "Yleiskaava"]) - self.layout.addWidget(self.type_label) - self.layout.addWidget(self.type_combo) - - self.submit_button = QPushButton("Submit", self) - self.submit_button.clicked.connect(self.on_submit) - self.layout.addWidget(self.submit_button) - - self.setLayout(self.layout) - - self.plan_id = None - self.plan_name = None - self.plan_type = None - - # Maybe the template should be read here and allow setting values in this dialog? - # So open additional fields in the dialog based on the type of plan selected. - - def on_submit(self): - self.plan_id = self.id_input.text() - self.plan_name = self.name_input.text() - # ToDo! plan_type should define which template is used. - self.plan_type = self.type_combo.currentText() - - if not self.plan_id or not self.plan_name: - QMessageBox.warning(self, "Input Error", "Both ID and Name fields must be filled out.") - return - - self.accept() \ No newline at end of file diff --git a/arho_feature_template/SimpleDrawTool.py b/arho_feature_template/SimpleDrawTool.py deleted file mode 100644 index e3bfddb..0000000 --- a/arho_feature_template/SimpleDrawTool.py +++ /dev/null @@ -1,16 +0,0 @@ -from qgis.gui import QgsMapToolDigitizeFeature, QgsMapToolCapture -from qgis.core import QgsProject -from qgis.utils import iface - -class SimpleDrawTool(QgsMapToolDigitizeFeature): - def __init__(self, canvas): - # Properly initializing with the required parameters - super().__init__(canvas, None, QgsMapToolCapture.CapturePolygon) - - self.canvas = canvas - self.layer = QgsProject.instance().mapLayersByName("Kaava")[0] - iface.setActiveLayer(self.layer) - - def addFeature(self, f): - self.layer.addFeature(f) - self.layer.triggerRepaint() \ No newline at end of file diff --git a/arho_feature_template/access_violation_plugin.txt b/arho_feature_template/access_violation_plugin.txt deleted file mode 100644 index 7c76836..0000000 --- a/arho_feature_template/access_violation_plugin.txt +++ /dev/null @@ -1,58 +0,0 @@ -from qgis.gui import QgsDockWidget, QgsMapToolDigitizeFeature, QgsAdvancedDigitizingDockWidget, QgsMapToolCapture - -def start_digitizing(self) -> None: - """Create a new QGIS project, add a layer group, create a new empty vector layer for digitizing, and add an OSM layer.""" - - # 1. Create a new project - project = QgsProject.instance() - project.clear() # Clear the current project (like starting a new one) - - # 2. Create a new group in the layer tree - group_name = "uusi kaava" - root = project.layerTreeRoot() - group = root.addGroup(group_name) - - # 3. Create a new empty vector layer (polygon layer for digitizing land use areas) - layer_name = "New Land Use Plan" - crs = "EPSG:4326" # Define the CRS (EPSG:4326 is WGS84, you can use any appropriate CRS) - new_layer = QgsVectorLayer(f"Polygon?crs={crs}", layer_name, "memory") - - if not new_layer.isValid(): - iface.messageBar().pushMessage("Error", "Failed to create new layer", level=3) - return - - # 4. Add the new layer to the project inside the "uusi kaava" group - QgsProject.instance().addMapLayer(new_layer, False) # Add layer to project but don't display it yet - group.insertLayer(0, new_layer) # Insert layer into the group at the first position - - # 5. Set the newly created layer as the active layer so digitizing can start - iface.setActiveLayer(new_layer) - - # 6. Start editing the layer (so the user can begin digitizing) - new_layer.startEditing() - - # 7. Add OpenStreetMap layer - osm_url = "type=xyz&url=https://tile.openstreetmap.org/{z}/{x}/{y}.png" - osm_layer = QgsRasterLayer(osm_url, "OpenStreetMap", "wms") - - if not osm_layer.isValid(): - iface.messageBar().pushMessage("Error", "Failed to add OpenStreetMap layer", level=3) - return - - # Add the OSM layer to the project - QgsProject.instance().addMapLayer(osm_layer) - - # 8. Create an instance of the advanced digitizing dock widget - advanced_digitizing_dock_widget = QgsAdvancedDigitizingDockWidget(iface.mapCanvas(), None) - - # 9. Create the digitizing tool - digitizing_tool = QgsMapToolDigitizeFeature(iface.mapCanvas(), advanced_digitizing_dock_widget, QgsMapToolCapture.CapturePolygon) - - # Ensure the layer is set correctly - digitizing_tool.setLayer(new_layer) # Set the layer to digitize - - # Set the map tool - iface.mapCanvas().setMapTool(digitizing_tool) - - # Notify the user that digitizing is ready - iface.messageBar().pushMessage("Info", f"New project created. Layer '{layer_name}' is ready for digitizing, and OpenStreetMap layer has been added.", level=0) \ No newline at end of file diff --git a/arho_feature_template/core/create_land_use_plan.py b/arho_feature_template/core/create_land_use_plan.py new file mode 100644 index 0000000..fbe838f --- /dev/null +++ b/arho_feature_template/core/create_land_use_plan.py @@ -0,0 +1,65 @@ +from qgis.core import QgsFeature, QgsGeometry, QgsVectorLayer +from qgis.PyQt.QtGui import QStandardItemModel +from qgis.utils import iface + +from .feature_template_library import TemplateGeometryDigitizeMapTool + + +class LandUsePlanTemplater: + def __init__(self): + self.active_template = None + self.library_configs = {} + self.template_model = QStandardItemModel() + + self.digitize_map_tool = TemplateGeometryDigitizeMapTool(iface.mapCanvas(), iface.cadDockWidget()) + self.digitize_map_tool.digitizingCompleted.connect(self.create_feature) + + def start_digitizing_for_layer(self, layer: QgsVectorLayer): + self.digitize_map_tool.clean() + self.digitize_map_tool.setLayer(layer) + iface.mapCanvas().setMapTool(self.digitize_map_tool) + + def create_feature(self, feature): + """Creates a new feature using stored dialog attributes.""" + print("create_feature called with feature:", feature) + + if not self.plan_id or not self.plan_name or not self.plan_type: + print("Plan information not available") + return + + layer = self.digitize_map_tool.layer() + + if not layer: + print("Layer is not set for digitizing.") + return + + print("Layer found:", layer.name(), "Editable:", layer.isEditable()) + new_feature = QgsFeature(layer.fields()) + + geometry = feature.geometry() + if not isinstance(geometry, QgsGeometry): + print("Expected geometry to be of type QgsGeometry, got:", type(geometry)) + return + + new_feature.setGeometry(geometry) + new_feature.setAttribute("id", self.plan_id) + new_feature.setAttribute("name", self.plan_name) + + if self.plan_type == "Detailed land use plan": + plan_type_value = 1 + elif self.plan_type == "General land use plan": + plan_type_value = 2 + elif self.plan_type == "Local land use plan": + plan_type_value = 3 + + new_feature.setAttribute("plan_type", plan_type_value) + + if not layer.isEditable(): + layer.startEditing() + + layer.beginEditCommand("Create land use plan feature.") + if layer.addFeature(new_feature): + print(f"Feature with ID {self.plan_id}, name {self.plan_name}, and type {plan_type_value} created successfully.") + else: + print("Failed to add feature to the layer.") + layer.commitChanges() diff --git a/arho_feature_template/core/update_plan.py b/arho_feature_template/core/update_plan.py new file mode 100644 index 0000000..2d0314c --- /dev/null +++ b/arho_feature_template/core/update_plan.py @@ -0,0 +1,53 @@ +from dataclasses import dataclass +from typing import List + +from qgis.core import QgsMapLayer, QgsProject, QgsVectorLayer + + +# To be extended and moved +@dataclass +class LandUsePlan: + id: str + name: str + +# To be replaced later +LAYER_PLAN_ID_MAP = { + "land_use_plan": "id", + "land_use_area": "plan_id", + "Osa-alue": "plan_id", +} + +def update_selected_plan(new_plan: LandUsePlan): + """Update the project layers based on the selected land use plan.""" + id = new_plan.id + for layer_name, field_name in LAYER_PLAN_ID_MAP.items(): + set_filter_for_vector_layer(layer_name, field_name, id) + +def set_filter_for_vector_layer(layer_name: str, field_name: str, field_value: str): + # Get layer and perform checks + layers = QgsProject.instance().mapLayersByName(layer_name) + if not _check_layer_count(layers): + return + layer = layers[0] + if not _check_vector_layer(layer): + return + + # Perform the filtering + query = f"{field_name} = {field_value}" + if not layer.setSubsetString(query): + # TODO: Convert to log msg? + print(f"ERROR: Failed to filter layer {layer_name} with query {query}") + +def _check_layer_count(layers: list) -> bool: + """Check if any layers are returned.""" + if not layers: + print("ERROR: No layers found with the specified name.") + return False + return True + +def _check_vector_layer(layer: QgsMapLayer) -> bool: + """Check if the given layer is a vector layer.""" + if not isinstance(layer, QgsVectorLayer): + print(f"ERROR: Layer {layer.name()} is not a vector layer: {type(layer)}") + return False + return True diff --git a/arho_feature_template/gui/new_land_use_plan_form.py b/arho_feature_template/gui/new_land_use_plan_form.py new file mode 100644 index 0000000..b8ac786 --- /dev/null +++ b/arho_feature_template/gui/new_land_use_plan_form.py @@ -0,0 +1,37 @@ +from collections import defaultdict +from importlib import resources + +from qgis.PyQt import uic +from qgis.PyQt.QtWidgets import QDialog + +ui_path = resources.files(__package__) / "new_land_use_plan_form.ui" +FormClass, _ = uic.loadUiType(ui_path) + + +class NewLandUsePlanForm(QDialog, FormClass): # type: ignore + """Dialog for creating a new land use plan with ID, Name, and Type.""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setupUi(self) + + self.plan_widgets = defaultdict(dict) + + self.plan_widgets['id'] = self.lineEdit_plan_id + self.plan_widgets['name'] = self.lineEdit_plan_name + self.plan_widgets['type'] = self.comboBox_plan_type + + @property + def plan_id(self): + """Returns the entered Plan ID.""" + return self.lineEdit_plan_id.text() + + @property + def plan_name(self): + """Returns the entered Plan Name.""" + return self.lineEdit_plan_name.text() + + @property + def plan_type(self): + """Returns the selected Plan Type.""" + return self.comboBox_plan_type.currentText() diff --git a/arho_feature_template/gui/new_land_use_plan_form.ui b/arho_feature_template/gui/new_land_use_plan_form.ui new file mode 100644 index 0000000..f6f2ea9 --- /dev/null +++ b/arho_feature_template/gui/new_land_use_plan_form.ui @@ -0,0 +1,107 @@ + + + NewLandUsePlanForm + + + + 0 + 0 + 400 + 300 + + + + Create New Land Use Plan + + + + + + + + Plan ID: + + + + + + + + + + Plan Name: + + + + + + + + + + Plan Type: + + + + + + + + Detailed land use plan + + + + + General land use plan + + + + + Local land use plan + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + button_box + accepted() + NewLandUsePlanForm + accept() + + + button_box + rejected() + NewLandUsePlanForm + reject() + + + \ No newline at end of file diff --git a/arho_feature_template/plugin.py b/arho_feature_template/plugin.py index 99d06f6..03dc42d 100644 --- a/arho_feature_template/plugin.py +++ b/arho_feature_template/plugin.py @@ -1,23 +1,22 @@ from __future__ import annotations import os - from typing import TYPE_CHECKING, Callable, cast from qgis.core import QgsProject -from qgis.PyQt.QtCore import QCoreApplication, QTranslator, Qt +from qgis.PyQt.QtCore import QCoreApplication, Qt, QTranslator from qgis.PyQt.QtGui import QIcon -from qgis.PyQt.QtWidgets import QAction, QWidget, QDialog +from qgis.PyQt.QtWidgets import QAction, QWidget from qgis.utils import iface +from arho_feature_template.core.create_land_use_plan import LandUsePlanTemplater from arho_feature_template.core.feature_template_library import FeatureTemplater, TemplateGeometryDigitizeMapTool +from arho_feature_template.core.update_plan import LandUsePlan, update_selected_plan +from arho_feature_template.gui.new_land_use_plan_form import NewLandUsePlanForm from arho_feature_template.qgis_plugin_tools.tools.custom_logging import setup_logger, teardown_logger from arho_feature_template.qgis_plugin_tools.tools.i18n import setup_translation from arho_feature_template.qgis_plugin_tools.tools.resources import plugin_name - from arho_feature_template.utils.misc_utils import PLUGIN_PATH -from arho_feature_template.update_plan import update_selected_plan, _check_vector_layer, LandUsePlan -from arho_feature_template.LandUsePlanDialog import LandUsePlanDialog if TYPE_CHECKING: from qgis.gui import QgisInterface, QgsMapTool @@ -129,10 +128,8 @@ def add_action( def initGui(self) -> None: # noqa N802 self.templater = FeatureTemplater() - new_path = os.path.join(PLUGIN_PATH, "resources/icons/city.png") # A placeholder icon - # Land use icons created by Fusion5085 - Flaticon - load_path = os.path.join(PLUGIN_PATH, "resources/icons/folder.png") - # Open icons created by Smashicons - Flaticon + plan_icon_path = os.path.join(PLUGIN_PATH, "resources/icons/city.png") # A placeholder icon + load_icon_path = os.path.join(PLUGIN_PATH, "resources/icons/folder.png") # A placeholder icon iface.addDockWidget(Qt.RightDockWidgetArea, self.templater.template_dock) self.templater.template_dock.visibilityChanged.connect(self.dock_visibility_changed) @@ -150,21 +147,20 @@ def initGui(self) -> None: # noqa N802 add_to_toolbar=True, ) - # Add additional action for "Lisää kaava" - new_action = self.add_action( - new_path, - text="Lisää kaava", - triggered_callback=self.start_digitizing, - parent=iface.mainWindow(), # Use iface instead of self.iface + self.new_land_use_plan_action = self.add_action( + plan_icon_path, + "Create New Land Use Plan", + self.show_land_use_plan_dialog, + add_to_menu=True, add_to_toolbar=True, + status_tip="Create a new land use plan", ) - # Add additional action for "Avaa kaava" - load_action = self.add_action( - load_path, - text="Avaa kaava", - triggered_callback=self.open_plan, - parent=iface.mainWindow(), # Use iface instead of self.iface + self.load_land_use_plan_action = self.add_action( + load_icon_path, + text="Load existing land use plan", + triggered_callback=self.load_existing_land_use_plan, + parent=iface.mainWindow(), add_to_toolbar=True, ) @@ -172,51 +168,30 @@ def on_map_tool_changed(self, new_tool: QgsMapTool, old_tool: QgsMapTool) -> Non if not isinstance(new_tool, TemplateGeometryDigitizeMapTool): self.template_dock_action.setChecked(False) - def start_digitizing(self) -> None: - dialog = LandUsePlanDialog() - if dialog.exec_() != QDialog.Accepted: - return - - # Create the LandUsePlan object from user input - new_plan = LandUsePlan(id=dialog.plan_id, name=dialog.plan_name) - plan_type = dialog.plan_type - - update_selected_plan(new_plan) + def show_land_use_plan_dialog(self): + dialog = NewLandUsePlanForm() + if dialog.exec_(): + new_land_use_plan = LandUsePlan(id=dialog.plan_id, name=dialog.plan_name) + update_selected_plan(new_land_use_plan) - layer_name = "Kaava" + self.land_use_plan_templater = LandUsePlanTemplater() + self.land_use_plan_templater.plan_id = dialog.plan_id + self.land_use_plan_templater.plan_name = dialog.plan_name + self.land_use_plan_templater.plan_type = dialog.plan_type - layers = QgsProject.instance().mapLayersByName(layer_name) - kaava_layer = layers[0] - - if not kaava_layer: - iface.messageBar().pushMessage("Error", f"Layer '{layer_name}' not found in the project", level=3) - return + # TODO: fix hard coded layer name. + layers = QgsProject.instance().mapLayersByName("land_use_plan") + if not layers: + iface.messageBar().pushMessage("Error", "Layer 'land_use_plan' not found in the project", level=3) + return - # new_layer = layers[0] + land_use_plan_layer = layers[0] - if not _check_vector_layer(kaava_layer): - iface.messageBar().pushMessage("Error", f"Layer '{layer_name}' is not a valid vector layer", level=3) - return - - iface.setActiveLayer(kaava_layer) - if not kaava_layer.isEditable(): - kaava_layer.startEditing() - - # digitizing_tool = SimpleDrawTool(iface.mapCanvas()) - - # Set the custom draw tool as the active tool on the canvas - # iface.mapCanvas().setMapTool(digitizing_tool) - - # Notify the user - iface.messageBar().pushMessage( - "Info", - f"Layer '{layer_name}' is filtered by plan '{new_plan.name}' of type '{plan_type}' and ready for digitizing.", - level=0, - ) + if land_use_plan_layer: + self.land_use_plan_templater.start_digitizing_for_layer(land_use_plan_layer) - def open_plan(self) -> None: + def load_existing_land_use_plan(self) -> None: """Open existing land use plan.""" - pass def unload(self) -> None: """Removes the plugin menu item and icon from QGIS GUI.""" @@ -231,4 +206,4 @@ def dock_visibility_changed(self, visible: bool) -> None: # noqa: FBT001 self.template_dock_action.setChecked(visible) def toggle_template_dock(self, show: bool) -> None: # noqa: FBT001 - self.templater.template_dock.setUserVisible(show) \ No newline at end of file + self.templater.template_dock.setUserVisible(show) diff --git a/arho_feature_template/update_plan.py b/arho_feature_template/update_plan.py deleted file mode 100644 index 7311453..0000000 --- a/arho_feature_template/update_plan.py +++ /dev/null @@ -1,105 +0,0 @@ -from dataclasses import dataclass - -from typing import List - -from qgis.core import QgsProject, QgsVectorLayer, QgsMapLayer - - -# To be extended and moved -@dataclass -class LandUsePlan: - id: str - name: str - - -# To be replaced later -LAYER_PLAN_ID_MAP = { - "Kaava": "id", - "land_use_area": "plan_id", - "Osa-alue": "plan_id", -} - -def update_selected_plan(new_plan: LandUsePlan): - """Update the project layers based on the selected land use plan.""" - id = new_plan.id - for layer_name, field_name in LAYER_PLAN_ID_MAP.items(): - set_filter_for_vector_layer(layer_name, field_name, id) - - -def set_filter_for_vector_layer(layer_name: str, field_name: str, field_value: str): - """Get the layer by name, check validity, and apply a filter based on the field value.""" - # Get the layer(s) by name - layers = QgsProject.instance().mapLayersByName(layer_name) - - # Ensure at least one layer was found - if not _check_layer_count(layers): - return - - # Access the first layer from the list - layer = layers[0] - - # Check if the layer is a valid vector layer - if not _check_vector_layer(layer): - return - - # Apply filtering logic here since the layer is valid - print(f"Layer {layer.name()} is valid for filtering.") - # Further operations like setting filter or using this layer for processing - -def _check_layer_count(layers: list) -> bool: - """Check if any layers are returned.""" - if not layers: - print(f"ERROR: No layers found with the specified name.") - return False - return True - - -def _check_vector_layer(layer: QgsMapLayer) -> bool: - """Check if the given layer is a vector layer.""" - if not isinstance(layer, QgsVectorLayer): - print(f"ERROR: Layer {layer.name()} is not a vector layer: {type(layer)}") - return False - return True - -# def update_selected_plan(new_plan: LandUsePlan): - # id = new_plan.id - # for layer_name, field_name in LAYER_PLAN_ID_MAP.items(): - # set_filter_for_vector_layer(layer_name, field_name, id) - - -# def set_filter_for_vector_layer(layer_name: str, field_name: str, field_value: str): - # Get layer and perform checks - # layers = QgsProject.instance().mapLayersByName(layer_name) - # if not _check_layer_count(layers): - # return - # layer = layers[0] - # if not _check_vector_layer(layer): - # return - - # Check if the layer is empty - # if layer.featureCount() == 0: - # print(f"INFO: Layer '{layer_name}' is empty. No features to filter.") - # return # Simply return without applying any filter - - # else: - # Perform the filtering - # query = f"{field_name} = {field_value}" - # if not layer.setSubsetString(query): - # TODO: Convert to log msg? - # print(f"ERROR: Failed to filter layer {layer_name} with query {query}") - -# def _check_layer_count(layers: List[QgsMapLayer]) -> bool: - # if len(layers) > 1: - # TODO: Convert to log msg? - # print(f"ERROR: Found multiple layers ({len(layers)}) with same name ({layers[0].name()}).") - # return False - # return True - - -# def _check_vector_layer(layer: QgsMapLayer) -> bool: - # if not isinstance(layer, QgsVectorLayer): - # TODO: Convert to log msg? - # print(f"ERROR: Layer {layer.name()} is not a vector layer: f{type(layer)}") - # print(f"Error this is not a vector layer! {layer}") - # return False - # return True \ No newline at end of file diff --git a/arho_feature_template/utils/__init__.py b/arho_feature_template/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/arho_feature_template/utils/misc_utils.py b/arho_feature_template/utils/misc_utils.py index 3e5a741..42f38d6 100644 --- a/arho_feature_template/utils/misc_utils.py +++ b/arho_feature_template/utils/misc_utils.py @@ -1,3 +1,3 @@ import os -PLUGIN_PATH = os.path.dirname(os.path.dirname(__file__)) \ No newline at end of file +PLUGIN_PATH = os.path.dirname(os.path.dirname(__file__)) From 5ff89f80e950a50d22bf136ebc52cc2b09f4c5e7 Mon Sep 17 00:00:00 2001 From: Miikka Kallio Date: Wed, 9 Oct 2024 12:36:14 +0300 Subject: [PATCH 3/8] Ruff-format fixes --- .../core/create_land_use_plan.py | 12 ++---------- arho_feature_template/core/update_plan.py | 17 +++++++++++------ .../gui/new_land_use_plan_form.py | 6 +++--- .../gui/new_land_use_plan_form.ui | 2 +- 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/arho_feature_template/core/create_land_use_plan.py b/arho_feature_template/core/create_land_use_plan.py index fbe838f..982d3cd 100644 --- a/arho_feature_template/core/create_land_use_plan.py +++ b/arho_feature_template/core/create_land_use_plan.py @@ -2,7 +2,7 @@ from qgis.PyQt.QtGui import QStandardItemModel from qgis.utils import iface -from .feature_template_library import TemplateGeometryDigitizeMapTool +from arho_feature_template.core.feature_template_library import TemplateGeometryDigitizeMapTool class LandUsePlanTemplater: @@ -21,24 +21,19 @@ def start_digitizing_for_layer(self, layer: QgsVectorLayer): def create_feature(self, feature): """Creates a new feature using stored dialog attributes.""" - print("create_feature called with feature:", feature) if not self.plan_id or not self.plan_name or not self.plan_type: - print("Plan information not available") return layer = self.digitize_map_tool.layer() if not layer: - print("Layer is not set for digitizing.") return - print("Layer found:", layer.name(), "Editable:", layer.isEditable()) new_feature = QgsFeature(layer.fields()) geometry = feature.geometry() if not isinstance(geometry, QgsGeometry): - print("Expected geometry to be of type QgsGeometry, got:", type(geometry)) return new_feature.setGeometry(geometry) @@ -58,8 +53,5 @@ def create_feature(self, feature): layer.startEditing() layer.beginEditCommand("Create land use plan feature.") - if layer.addFeature(new_feature): - print(f"Feature with ID {self.plan_id}, name {self.plan_name}, and type {plan_type_value} created successfully.") - else: - print("Failed to add feature to the layer.") + layer.addFeature(new_feature) layer.commitChanges() diff --git a/arho_feature_template/core/update_plan.py b/arho_feature_template/core/update_plan.py index 2d0314c..93342bf 100644 --- a/arho_feature_template/core/update_plan.py +++ b/arho_feature_template/core/update_plan.py @@ -1,7 +1,7 @@ from dataclasses import dataclass -from typing import List from qgis.core import QgsMapLayer, QgsProject, QgsVectorLayer +from qgis.utils import iface # To be extended and moved @@ -10,6 +10,7 @@ class LandUsePlan: id: str name: str + # To be replaced later LAYER_PLAN_ID_MAP = { "land_use_plan": "id", @@ -17,11 +18,13 @@ class LandUsePlan: "Osa-alue": "plan_id", } + def update_selected_plan(new_plan: LandUsePlan): """Update the project layers based on the selected land use plan.""" - id = new_plan.id + plan_id = new_plan.id for layer_name, field_name in LAYER_PLAN_ID_MAP.items(): - set_filter_for_vector_layer(layer_name, field_name, id) + set_filter_for_vector_layer(layer_name, field_name, plan_id) + def set_filter_for_vector_layer(layer_name: str, field_name: str, field_value: str): # Get layer and perform checks @@ -36,18 +39,20 @@ def set_filter_for_vector_layer(layer_name: str, field_name: str, field_value: s query = f"{field_name} = {field_value}" if not layer.setSubsetString(query): # TODO: Convert to log msg? - print(f"ERROR: Failed to filter layer {layer_name} with query {query}") + iface.messageBar().pushMessage("Error", f"Failed to filter layer {layer_name} with query {query}", level=3) + def _check_layer_count(layers: list) -> bool: """Check if any layers are returned.""" if not layers: - print("ERROR: No layers found with the specified name.") + iface.messageBar().pushMessage("Error", "ERROR: No layers found with the specified name.", level=3) return False return True + def _check_vector_layer(layer: QgsMapLayer) -> bool: """Check if the given layer is a vector layer.""" if not isinstance(layer, QgsVectorLayer): - print(f"ERROR: Layer {layer.name()} is not a vector layer: {type(layer)}") + iface.messageBar().pushMessage("Error", f"Layer {layer.name()} is not a vector layer: {type(layer)}", level=3) return False return True diff --git a/arho_feature_template/gui/new_land_use_plan_form.py b/arho_feature_template/gui/new_land_use_plan_form.py index b8ac786..7c416c7 100644 --- a/arho_feature_template/gui/new_land_use_plan_form.py +++ b/arho_feature_template/gui/new_land_use_plan_form.py @@ -17,9 +17,9 @@ def __init__(self, parent=None): self.plan_widgets = defaultdict(dict) - self.plan_widgets['id'] = self.lineEdit_plan_id - self.plan_widgets['name'] = self.lineEdit_plan_name - self.plan_widgets['type'] = self.comboBox_plan_type + self.plan_widgets["id"] = self.lineEdit_plan_id + self.plan_widgets["name"] = self.lineEdit_plan_name + self.plan_widgets["type"] = self.comboBox_plan_type @property def plan_id(self): diff --git a/arho_feature_template/gui/new_land_use_plan_form.ui b/arho_feature_template/gui/new_land_use_plan_form.ui index f6f2ea9..e94ac1f 100644 --- a/arho_feature_template/gui/new_land_use_plan_form.ui +++ b/arho_feature_template/gui/new_land_use_plan_form.ui @@ -104,4 +104,4 @@ reject() - \ No newline at end of file + From 63164e69ae586ead846e4cd31d64b8d007b6182c Mon Sep 17 00:00:00 2001 From: Miikka Kallio Date: Wed, 9 Oct 2024 12:54:32 +0300 Subject: [PATCH 4/8] Enable layer editing before digitizing geometry. --- arho_feature_template/plugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arho_feature_template/plugin.py b/arho_feature_template/plugin.py index 03dc42d..2752cd7 100644 --- a/arho_feature_template/plugin.py +++ b/arho_feature_template/plugin.py @@ -188,6 +188,8 @@ def show_land_use_plan_dialog(self): land_use_plan_layer = layers[0] if land_use_plan_layer: + if not land_use_plan_layer.isEditable(): + land_use_plan_layer.startEditing() self.land_use_plan_templater.start_digitizing_for_layer(land_use_plan_layer) def load_existing_land_use_plan(self) -> None: From 36f1fbfe5c56d1c98c0deca566d54dbdd3f1e91a Mon Sep 17 00:00:00 2001 From: Miikka Kallio Date: Fri, 18 Oct 2024 00:03:22 +0300 Subject: [PATCH 5/8] Removed custom form for creating AddFeature instead. new plan, use Use default add feature to create new plan. Form used to define feature comes from QGIS-project. --- .../gui/new_land_use_plan_form.py | 37 ------ .../gui/new_land_use_plan_form.ui | 107 ------------------ arho_feature_template/plugin.py | 67 ++++++----- 3 files changed, 37 insertions(+), 174 deletions(-) delete mode 100644 arho_feature_template/gui/new_land_use_plan_form.py delete mode 100644 arho_feature_template/gui/new_land_use_plan_form.ui diff --git a/arho_feature_template/gui/new_land_use_plan_form.py b/arho_feature_template/gui/new_land_use_plan_form.py deleted file mode 100644 index 7c416c7..0000000 --- a/arho_feature_template/gui/new_land_use_plan_form.py +++ /dev/null @@ -1,37 +0,0 @@ -from collections import defaultdict -from importlib import resources - -from qgis.PyQt import uic -from qgis.PyQt.QtWidgets import QDialog - -ui_path = resources.files(__package__) / "new_land_use_plan_form.ui" -FormClass, _ = uic.loadUiType(ui_path) - - -class NewLandUsePlanForm(QDialog, FormClass): # type: ignore - """Dialog for creating a new land use plan with ID, Name, and Type.""" - - def __init__(self, parent=None): - super().__init__(parent) - self.setupUi(self) - - self.plan_widgets = defaultdict(dict) - - self.plan_widgets["id"] = self.lineEdit_plan_id - self.plan_widgets["name"] = self.lineEdit_plan_name - self.plan_widgets["type"] = self.comboBox_plan_type - - @property - def plan_id(self): - """Returns the entered Plan ID.""" - return self.lineEdit_plan_id.text() - - @property - def plan_name(self): - """Returns the entered Plan Name.""" - return self.lineEdit_plan_name.text() - - @property - def plan_type(self): - """Returns the selected Plan Type.""" - return self.comboBox_plan_type.currentText() diff --git a/arho_feature_template/gui/new_land_use_plan_form.ui b/arho_feature_template/gui/new_land_use_plan_form.ui deleted file mode 100644 index e94ac1f..0000000 --- a/arho_feature_template/gui/new_land_use_plan_form.ui +++ /dev/null @@ -1,107 +0,0 @@ - - - NewLandUsePlanForm - - - - 0 - 0 - 400 - 300 - - - - Create New Land Use Plan - - - - - - - - Plan ID: - - - - - - - - - - Plan Name: - - - - - - - - - - Plan Type: - - - - - - - - Detailed land use plan - - - - - General land use plan - - - - - Local land use plan - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - button_box - accepted() - NewLandUsePlanForm - accept() - - - button_box - rejected() - NewLandUsePlanForm - reject() - - - diff --git a/arho_feature_template/plugin.py b/arho_feature_template/plugin.py index 2752cd7..590a766 100644 --- a/arho_feature_template/plugin.py +++ b/arho_feature_template/plugin.py @@ -3,16 +3,15 @@ import os from typing import TYPE_CHECKING, Callable, cast -from qgis.core import QgsProject +from qgis.core import QgsProject, QgsWkbTypes from qgis.PyQt.QtCore import QCoreApplication, Qt, QTranslator from qgis.PyQt.QtGui import QIcon from qgis.PyQt.QtWidgets import QAction, QWidget from qgis.utils import iface -from arho_feature_template.core.create_land_use_plan import LandUsePlanTemplater from arho_feature_template.core.feature_template_library import FeatureTemplater, TemplateGeometryDigitizeMapTool -from arho_feature_template.core.update_plan import LandUsePlan, update_selected_plan -from arho_feature_template.gui.new_land_use_plan_form import NewLandUsePlanForm + +# from arho_feature_template.core.update_plan import LandUsePlan, update_selected_plan from arho_feature_template.qgis_plugin_tools.tools.custom_logging import setup_logger, teardown_logger from arho_feature_template.qgis_plugin_tools.tools.i18n import setup_translation from arho_feature_template.qgis_plugin_tools.tools.resources import plugin_name @@ -149,11 +148,11 @@ def initGui(self) -> None: # noqa N802 self.new_land_use_plan_action = self.add_action( plan_icon_path, - "Create New Land Use Plan", - self.show_land_use_plan_dialog, + "Create New Plan", + self.digitize_new_plan, add_to_menu=True, add_to_toolbar=True, - status_tip="Create a new land use plan", + status_tip="Create a new plan", ) self.load_land_use_plan_action = self.add_action( @@ -168,29 +167,37 @@ def on_map_tool_changed(self, new_tool: QgsMapTool, old_tool: QgsMapTool) -> Non if not isinstance(new_tool, TemplateGeometryDigitizeMapTool): self.template_dock_action.setChecked(False) - def show_land_use_plan_dialog(self): - dialog = NewLandUsePlanForm() - if dialog.exec_(): - new_land_use_plan = LandUsePlan(id=dialog.plan_id, name=dialog.plan_name) - update_selected_plan(new_land_use_plan) - - self.land_use_plan_templater = LandUsePlanTemplater() - self.land_use_plan_templater.plan_id = dialog.plan_id - self.land_use_plan_templater.plan_name = dialog.plan_name - self.land_use_plan_templater.plan_type = dialog.plan_type - - # TODO: fix hard coded layer name. - layers = QgsProject.instance().mapLayersByName("land_use_plan") - if not layers: - iface.messageBar().pushMessage("Error", "Layer 'land_use_plan' not found in the project", level=3) - return - - land_use_plan_layer = layers[0] - - if land_use_plan_layer: - if not land_use_plan_layer.isEditable(): - land_use_plan_layer.startEditing() - self.land_use_plan_templater.start_digitizing_for_layer(land_use_plan_layer) + def digitize_new_plan(self): + # Activate and start editing the Kaava-layer + layers = QgsProject.instance().mapLayersByName("Kaava") + if not layers: + iface.messageBar().pushMessage("Error", "Layer 'Kaava' not found", level=3) + return + + kaava_layer = layers[0] + + if not kaava_layer.isEditable(): + kaava_layer.startEditing() + + iface.setActiveLayer(kaava_layer) + + if kaava_layer.geometryType() != QgsWkbTypes.PolygonGeometry: + iface.messageBar().pushMessage("Error", "Layer 'Kaava' is not a polygon layer", level=3) + return + + kaava_layer.featureAdded.connect(self.commit_new_plan) + + iface.actionAddFeature().trigger() + + def commit_new_plan(self): + kaava_layer = iface.activeLayer() + + kaava_layer.featureAdded.disconnect() + + if kaava_layer.commitChanges(): + iface.messageBar().pushMessage("Info", "Feature committed successfully", level=0) + else: + iface.messageBar().pushMessage("Error", "Failed to commit feature", level=3) def load_existing_land_use_plan(self) -> None: """Open existing land use plan.""" From 61ac0fa5ab5a0156628ab9c410f84ab971fc3fbf Mon Sep 17 00:00:00 2001 From: Miikka Kallio Date: Mon, 21 Oct 2024 02:52:24 +0300 Subject: [PATCH 6/8] FIlter layers after new plan is committed. After committing new plan feature, filter layers based on the id of the new plan. The id is retrieved by comparing ids before and after commit. --- .../core/create_land_use_plan.py | 57 ------------------- arho_feature_template/core/update_plan.py | 27 +++++---- arho_feature_template/plugin.py | 53 +++++++++++------ 3 files changed, 53 insertions(+), 84 deletions(-) delete mode 100644 arho_feature_template/core/create_land_use_plan.py diff --git a/arho_feature_template/core/create_land_use_plan.py b/arho_feature_template/core/create_land_use_plan.py deleted file mode 100644 index 982d3cd..0000000 --- a/arho_feature_template/core/create_land_use_plan.py +++ /dev/null @@ -1,57 +0,0 @@ -from qgis.core import QgsFeature, QgsGeometry, QgsVectorLayer -from qgis.PyQt.QtGui import QStandardItemModel -from qgis.utils import iface - -from arho_feature_template.core.feature_template_library import TemplateGeometryDigitizeMapTool - - -class LandUsePlanTemplater: - def __init__(self): - self.active_template = None - self.library_configs = {} - self.template_model = QStandardItemModel() - - self.digitize_map_tool = TemplateGeometryDigitizeMapTool(iface.mapCanvas(), iface.cadDockWidget()) - self.digitize_map_tool.digitizingCompleted.connect(self.create_feature) - - def start_digitizing_for_layer(self, layer: QgsVectorLayer): - self.digitize_map_tool.clean() - self.digitize_map_tool.setLayer(layer) - iface.mapCanvas().setMapTool(self.digitize_map_tool) - - def create_feature(self, feature): - """Creates a new feature using stored dialog attributes.""" - - if not self.plan_id or not self.plan_name or not self.plan_type: - return - - layer = self.digitize_map_tool.layer() - - if not layer: - return - - new_feature = QgsFeature(layer.fields()) - - geometry = feature.geometry() - if not isinstance(geometry, QgsGeometry): - return - - new_feature.setGeometry(geometry) - new_feature.setAttribute("id", self.plan_id) - new_feature.setAttribute("name", self.plan_name) - - if self.plan_type == "Detailed land use plan": - plan_type_value = 1 - elif self.plan_type == "General land use plan": - plan_type_value = 2 - elif self.plan_type == "Local land use plan": - plan_type_value = 3 - - new_feature.setAttribute("plan_type", plan_type_value) - - if not layer.isEditable(): - layer.startEditing() - - layer.beginEditCommand("Create land use plan feature.") - layer.addFeature(new_feature) - layer.commitChanges() diff --git a/arho_feature_template/core/update_plan.py b/arho_feature_template/core/update_plan.py index 93342bf..640a5ef 100644 --- a/arho_feature_template/core/update_plan.py +++ b/arho_feature_template/core/update_plan.py @@ -8,13 +8,15 @@ @dataclass class LandUsePlan: id: str - name: str # To be replaced later LAYER_PLAN_ID_MAP = { - "land_use_plan": "id", - "land_use_area": "plan_id", + "Kaava": "id", + "Maankäytön kohteet": "plan_id", + "Muut pisteet": "plan_id", + "Viivat": "plan_id", + "Aluevaraus": "plan_id", "Osa-alue": "plan_id", } @@ -22,24 +24,27 @@ class LandUsePlan: def update_selected_plan(new_plan: LandUsePlan): """Update the project layers based on the selected land use plan.""" plan_id = new_plan.id + for layer_name, field_name in LAYER_PLAN_ID_MAP.items(): + # Set the filter on each layer using the plan_id set_filter_for_vector_layer(layer_name, field_name, plan_id) def set_filter_for_vector_layer(layer_name: str, field_name: str, field_value: str): - # Get layer and perform checks + """Set a filter for the given vector layer.""" layers = QgsProject.instance().mapLayersByName(layer_name) + if not _check_layer_count(layers): return + layer = layers[0] - if not _check_vector_layer(layer): - return - # Perform the filtering - query = f"{field_name} = {field_value}" - if not layer.setSubsetString(query): - # TODO: Convert to log msg? - iface.messageBar().pushMessage("Error", f"Failed to filter layer {layer_name} with query {query}", level=3) + # Create the filter expression directly based on the plan_id + expression = f"\"{field_name}\" = '{field_value}'" # Properly formatted filter expression + + # Apply the filter to the layer + if not layer.setSubsetString(expression): + iface.messageBar().pushMessage("Error", f"Failed to filter layer {layer_name} with query {expression}", level=3) def _check_layer_count(layers: list) -> bool: diff --git a/arho_feature_template/plugin.py b/arho_feature_template/plugin.py index 590a766..124a5f9 100644 --- a/arho_feature_template/plugin.py +++ b/arho_feature_template/plugin.py @@ -3,15 +3,14 @@ import os from typing import TYPE_CHECKING, Callable, cast -from qgis.core import QgsProject, QgsWkbTypes +from qgis.core import QgsProject, QgsVectorLayer from qgis.PyQt.QtCore import QCoreApplication, Qt, QTranslator from qgis.PyQt.QtGui import QIcon from qgis.PyQt.QtWidgets import QAction, QWidget from qgis.utils import iface from arho_feature_template.core.feature_template_library import FeatureTemplater, TemplateGeometryDigitizeMapTool - -# from arho_feature_template.core.update_plan import LandUsePlan, update_selected_plan +from arho_feature_template.core.update_plan import LandUsePlan, update_selected_plan from arho_feature_template.qgis_plugin_tools.tools.custom_logging import setup_logger, teardown_logger from arho_feature_template.qgis_plugin_tools.tools.i18n import setup_translation from arho_feature_template.qgis_plugin_tools.tools.resources import plugin_name @@ -135,7 +134,6 @@ def initGui(self) -> None: # noqa N802 iface.mapCanvas().mapToolSet.connect(self.templater.digitize_map_tool.deactivate) - # Add main plugin action to the toolbar self.template_dock_action = self.add_action( "", "Feature Templates", @@ -167,8 +165,18 @@ def on_map_tool_changed(self, new_tool: QgsMapTool, old_tool: QgsMapTool) -> Non if not isinstance(new_tool, TemplateGeometryDigitizeMapTool): self.template_dock_action.setChecked(False) + def clear_all_filters(self): + """Clear filters for all vector layers in the project.""" + layers = QgsProject.instance().mapLayers().values() + + for layer in layers: + if isinstance(layer, QgsVectorLayer): + layer.setSubsetString("") + def digitize_new_plan(self): - # Activate and start editing the Kaava-layer + # Filtered layers are not editable, so clear filters first. + self.clear_all_filters() + # Find and set the "Kaava" layer layers = QgsProject.instance().mapLayersByName("Kaava") if not layers: iface.messageBar().pushMessage("Error", "Layer 'Kaava' not found", level=3) @@ -181,23 +189,36 @@ def digitize_new_plan(self): iface.setActiveLayer(kaava_layer) - if kaava_layer.geometryType() != QgsWkbTypes.PolygonGeometry: - iface.messageBar().pushMessage("Error", "Layer 'Kaava' is not a polygon layer", level=3) - return - - kaava_layer.featureAdded.connect(self.commit_new_plan) - iface.actionAddFeature().trigger() + kaava_layer.featureAdded.connect(self.feature_added) - def commit_new_plan(self): + def feature_added(self): kaava_layer = iface.activeLayer() + feature_ids_before_commit = kaava_layer.allFeatureIds() + if kaava_layer.isEditable(): + if not kaava_layer.commitChanges(): + iface.messageBar().pushMessage("Error", "Failed to commit changes to the layer.", level=3) + return + else: + iface.messageBar().pushMessage("Error", "Layer is not editable.", level=3) + return + feature_ids_after_commit = kaava_layer.allFeatureIds() + + # Finds the feature id that was committed by comparing ids before commit to features after commit. + new_feature_id = next((fid for fid in feature_ids_after_commit if fid not in feature_ids_before_commit), None) + if new_feature_id is not None: + new_feature = kaava_layer.getFeature(new_feature_id) - kaava_layer.featureAdded.disconnect() + if new_feature.isValid(): + feature_id_value = new_feature["id"] # UUID of the new feature + iface.messageBar().pushMessage("Info", f"Feature 'id' field value: {feature_id_value}", level=0) - if kaava_layer.commitChanges(): - iface.messageBar().pushMessage("Info", "Feature committed successfully", level=0) + # plan = LandUsePlan(feature_id_value) + update_selected_plan(LandUsePlan(feature_id_value)) + else: + iface.messageBar().pushMessage("Error", "Invalid feature retrieved.", level=3) else: - iface.messageBar().pushMessage("Error", "Failed to commit feature", level=3) + iface.messageBar().pushMessage("Error", "No new feature was added.", level=3) def load_existing_land_use_plan(self) -> None: """Open existing land use plan.""" From 203d2a5e8303ee1c296eeb0cab9b51d7d39b2304 Mon Sep 17 00:00:00 2001 From: Miikka Kallio Date: Tue, 22 Oct 2024 13:15:52 +0300 Subject: [PATCH 7/8] Disconnect featureAdded signal. Move code from plugin to new_plan. Disconnect featureAdded signal. Fixes commit errors. Moved code from plugin.py to new_plan.py --- arho_feature_template/core/new_plan.py | 64 +++++++++++++++++++ arho_feature_template/core/update_plan.py | 3 +- arho_feature_template/plugin.py | 58 +---------------- .../template_libraries/asemakaava-sample.yaml | 2 + 4 files changed, 70 insertions(+), 57 deletions(-) create mode 100644 arho_feature_template/core/new_plan.py diff --git a/arho_feature_template/core/new_plan.py b/arho_feature_template/core/new_plan.py new file mode 100644 index 0000000..df996b1 --- /dev/null +++ b/arho_feature_template/core/new_plan.py @@ -0,0 +1,64 @@ +from qgis.core import QgsProject, QgsVectorLayer +from qgis.utils import iface + +from arho_feature_template.core.update_plan import LandUsePlan, update_selected_plan + + +class NewPlan: + def add_new_plan(self): + # Filtered layers are not editable, so clear filters first. + self.clear_all_filters() + + layers = QgsProject.instance().mapLayersByName("Kaava") + if not layers: + iface.messageBar().pushMessage("Error", "Layer 'Kaava' not found", level=3) + return + + kaava_layer = layers[0] + + if not kaava_layer.isEditable(): + kaava_layer.startEditing() + + iface.setActiveLayer(kaava_layer) + + iface.actionAddFeature().trigger() + + # Connect the featureAdded signal + kaava_layer.featureAdded.connect(self.feature_added) + + + def feature_added(self): + kaava_layer = iface.activeLayer() + kaava_layer.featureAdded.disconnect() + feature_ids_before_commit = kaava_layer.allFeatureIds() + if kaava_layer.isEditable(): + if not kaava_layer.commitChanges(): + iface.messageBar().pushMessage("Error", "Failed to commit changes to the layer.", level=3) + return + else: + iface.messageBar().pushMessage("Error", "Layer is not editable.", level=3) + return + + feature_ids_after_commit = kaava_layer.allFeatureIds() + + # Find the new plan.id by comparing fids before and after commit. + new_feature_id = next((fid for fid in feature_ids_after_commit if fid not in feature_ids_before_commit), None) + + if new_feature_id is not None: + new_feature = kaava_layer.getFeature(new_feature_id) + + if new_feature.isValid(): + feature_id_value = new_feature["id"] + update_selected_plan(LandUsePlan(feature_id_value)) + else: + iface.messageBar().pushMessage("Error", "Invalid feature retrieved.", level=3) + else: + iface.messageBar().pushMessage("Error", "No new feature was added.", level=3) + + def clear_all_filters(self): + """Clear filters for all vector layers in the project.""" + layers = QgsProject.instance().mapLayers().values() + + for layer in layers: + if isinstance(layer, QgsVectorLayer): + layer.setSubsetString("") diff --git a/arho_feature_template/core/update_plan.py b/arho_feature_template/core/update_plan.py index 640a5ef..d083090 100644 --- a/arho_feature_template/core/update_plan.py +++ b/arho_feature_template/core/update_plan.py @@ -39,8 +39,7 @@ def set_filter_for_vector_layer(layer_name: str, field_name: str, field_value: s layer = layers[0] - # Create the filter expression directly based on the plan_id - expression = f"\"{field_name}\" = '{field_value}'" # Properly formatted filter expression + expression = f"\"{field_name}\" = '{field_value}'" # Apply the filter to the layer if not layer.setSubsetString(expression): diff --git a/arho_feature_template/plugin.py b/arho_feature_template/plugin.py index 124a5f9..e60d754 100644 --- a/arho_feature_template/plugin.py +++ b/arho_feature_template/plugin.py @@ -3,14 +3,13 @@ import os from typing import TYPE_CHECKING, Callable, cast -from qgis.core import QgsProject, QgsVectorLayer from qgis.PyQt.QtCore import QCoreApplication, Qt, QTranslator from qgis.PyQt.QtGui import QIcon from qgis.PyQt.QtWidgets import QAction, QWidget from qgis.utils import iface from arho_feature_template.core.feature_template_library import FeatureTemplater, TemplateGeometryDigitizeMapTool -from arho_feature_template.core.update_plan import LandUsePlan, update_selected_plan +from arho_feature_template.core.new_plan import NewPlan from arho_feature_template.qgis_plugin_tools.tools.custom_logging import setup_logger, teardown_logger from arho_feature_template.qgis_plugin_tools.tools.i18n import setup_translation from arho_feature_template.qgis_plugin_tools.tools.resources import plugin_name @@ -125,6 +124,7 @@ def add_action( def initGui(self) -> None: # noqa N802 self.templater = FeatureTemplater() + self.new_plan = NewPlan() plan_icon_path = os.path.join(PLUGIN_PATH, "resources/icons/city.png") # A placeholder icon load_icon_path = os.path.join(PLUGIN_PATH, "resources/icons/folder.png") # A placeholder icon @@ -165,60 +165,8 @@ def on_map_tool_changed(self, new_tool: QgsMapTool, old_tool: QgsMapTool) -> Non if not isinstance(new_tool, TemplateGeometryDigitizeMapTool): self.template_dock_action.setChecked(False) - def clear_all_filters(self): - """Clear filters for all vector layers in the project.""" - layers = QgsProject.instance().mapLayers().values() - - for layer in layers: - if isinstance(layer, QgsVectorLayer): - layer.setSubsetString("") - def digitize_new_plan(self): - # Filtered layers are not editable, so clear filters first. - self.clear_all_filters() - # Find and set the "Kaava" layer - layers = QgsProject.instance().mapLayersByName("Kaava") - if not layers: - iface.messageBar().pushMessage("Error", "Layer 'Kaava' not found", level=3) - return - - kaava_layer = layers[0] - - if not kaava_layer.isEditable(): - kaava_layer.startEditing() - - iface.setActiveLayer(kaava_layer) - - iface.actionAddFeature().trigger() - kaava_layer.featureAdded.connect(self.feature_added) - - def feature_added(self): - kaava_layer = iface.activeLayer() - feature_ids_before_commit = kaava_layer.allFeatureIds() - if kaava_layer.isEditable(): - if not kaava_layer.commitChanges(): - iface.messageBar().pushMessage("Error", "Failed to commit changes to the layer.", level=3) - return - else: - iface.messageBar().pushMessage("Error", "Layer is not editable.", level=3) - return - feature_ids_after_commit = kaava_layer.allFeatureIds() - - # Finds the feature id that was committed by comparing ids before commit to features after commit. - new_feature_id = next((fid for fid in feature_ids_after_commit if fid not in feature_ids_before_commit), None) - if new_feature_id is not None: - new_feature = kaava_layer.getFeature(new_feature_id) - - if new_feature.isValid(): - feature_id_value = new_feature["id"] # UUID of the new feature - iface.messageBar().pushMessage("Info", f"Feature 'id' field value: {feature_id_value}", level=0) - - # plan = LandUsePlan(feature_id_value) - update_selected_plan(LandUsePlan(feature_id_value)) - else: - iface.messageBar().pushMessage("Error", "Invalid feature retrieved.", level=3) - else: - iface.messageBar().pushMessage("Error", "No new feature was added.", level=3) + self.new_plan.add_new_plan() def load_existing_land_use_plan(self) -> None: """Open existing land use plan.""" diff --git a/arho_feature_template/resources/template_libraries/asemakaava-sample.yaml b/arho_feature_template/resources/template_libraries/asemakaava-sample.yaml index 00cdae8..b7901f8 100644 --- a/arho_feature_template/resources/template_libraries/asemakaava-sample.yaml +++ b/arho_feature_template/resources/template_libraries/asemakaava-sample.yaml @@ -7,6 +7,7 @@ templates: description: Kaavakohde ilman kikkareita feature: layer: land_use_area + # layer: Aluevaraus attributes: - attribute: name - attribute: type_of_underground_id @@ -15,6 +16,7 @@ templates: description: Aluella kuvataan ... feature: layer: land_use_area + # layer: Aluevaraus attributes: - attribute: name - attribute: type_of_underground_id From ef6c26c4e682168dfb19460e7307464699746b58 Mon Sep 17 00:00:00 2001 From: Miikka Kallio Date: Tue, 22 Oct 2024 13:22:41 +0300 Subject: [PATCH 8/8] Ruff format fix --- arho_feature_template/core/new_plan.py | 1 - 1 file changed, 1 deletion(-) diff --git a/arho_feature_template/core/new_plan.py b/arho_feature_template/core/new_plan.py index df996b1..97dd5ee 100644 --- a/arho_feature_template/core/new_plan.py +++ b/arho_feature_template/core/new_plan.py @@ -26,7 +26,6 @@ def add_new_plan(self): # Connect the featureAdded signal kaava_layer.featureAdded.connect(self.feature_added) - def feature_added(self): kaava_layer = iface.activeLayer() kaava_layer.featureAdded.disconnect()