From e514e7571e7bbb99cefbbb35063bec5390dac2f4 Mon Sep 17 00:00:00 2001 From: daniel Date: Tue, 2 Dec 2025 20:32:04 +0100 Subject: [PATCH 01/10] =?UTF-8?q?Stile=20und=20Linkliste=20aus=20Plan41=20?= =?UTF-8?q?=C3=BCbernommen,=20Ordner=20angelegt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/Linkliste.xlsx | Bin 0 -> 14640 bytes styles/GIS_63000F_Objekt_Denkmalschutz.qml | 609 +++++++++++++++++++++ styles/GIS_Flst_Beschriftung_ALKIS_NAS.qml | 349 ++++++++++++ styles/GIS_LfULG_LSG.qml | 371 +++++++++++++ 4 files changed, 1329 insertions(+) create mode 100644 assets/Linkliste.xlsx create mode 100644 styles/GIS_63000F_Objekt_Denkmalschutz.qml create mode 100644 styles/GIS_Flst_Beschriftung_ALKIS_NAS.qml create mode 100644 styles/GIS_LfULG_LSG.qml diff --git a/assets/Linkliste.xlsx b/assets/Linkliste.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..bb18ee5c065d2882e95c6f405ad6ac4b2cee8543 GIT binary patch literal 14640 zcmeHu1y^0k()K}uySuw5v^l zrh3F{l6~F7rh4(EG*?oqh3X+!vYFDMxZ%aBT&BUbJ-R+Niai$#{^A1Sdn2T5^Rh96HOU(ZIR<+J4C_J9wfvEAdANWGc?ZpY8R*mE z3ExPs7N`uhE0!^*N02f-j`X@ld%F?~t2$YAhBLkzL*hd;HCnA=OT!OBspHWZv|PPf zOKfftLP4tvkT13@gmA2(&`7n+XxBs*A0Ze?*IGxLSz!8>J$wvLB(PeucB;O*8oclX zPRD*y%T<`Gb8oTU{S14P=6rL?He6I0K|iW(b}Ih>{px{b=5uFHS9pUNFm*_j%^Z8> z*%1uo9ExPM23T)BL%zjxt^_@VzPeTCz`VqKPDF^A<>dUFxeQ2gH^Pjo;=Y3e4}d!r zak5;z|AhBczJjopl`llNnS6zS`wHaQ+Zz}_@jqzVsK!Kk3$#61pcEEJQv)Y68)rs_ zU-$pf^Zzg>|LtF|h?i67V}c94kbDUnewbT}MG}^A7m{ovR`vTRwTjpnT|kDr)2*+CgI=V_cFS&#v6S+Onkr1RuP4Q#zXqmy)rcAhodVb4TV#(xMRguKeF50-Q0be zgtRA>TgPXb^5!pjG9w$L;@_`CYmufHG;p9%i||4*_|gKj2j#TZjUTH(=7d$wDno0U zIC75@rZW5&l8cYv1;V*xPiNB5hnBXz zFaY2Q0sue+j*N#5qr1J+2V;Bt55Ly23N<~26(%&_oQ5}~CqA-C2^rGf+0Qj<#;7JX z-Vwp8{kC*?JoWh6!H*Yh@{-smut8##_|MBj2kYKd&euj7&Iidf6NQq<{MT_JV#G2O z@2P+Ix$YB?O5l=6_ZH<+w-(^|a{9iVo6^orNRQ$|0)pk0%w!WhvKeT!QGLm9X}ar@ z%P1g1t#^*Zm6VSol~tS>FMq~qpi(G)j{TBiIDD6m?m`^(wYc||DALLJV`r6S0c1m~ z2ehu-tlUa-NCoakP3FmQJ;=z+R>@>sVstG>2k$Twi;_)_K!N)_=+K027YEe|81hEl z4APJHUNexdCL4W^T2O9b%pTavyVPD|HV6+Dvxi9);@%b0s95(KpF)MSv(!gn*RtDf^dKaG!dT3-wQu|D0nH%X1SH-b839lpK~eKvLnV+@?;a zd~$~;*hk-^O;*S-7lIpMBrnYqys0Kk*=VI0$0fJrbkUz#X>VaoLi6lffk)+D*lyht z73HMVRw=jtp1 zEi(*_P%V~+;^Uh$V3hi9dVHeJ;Z%IdXJ6jpbvF9oyCIHzv4>wMVK+9rG>nwC3SU`| za0yl5EMY2t!kS@^-%QyKc_Z(OpN1~wIW8t)ljoGEMXSZb@G+Di z&8|&3-LF*QlV9uKOp8h@?q8thZ=G|fR#XLXhD_R!rTgP$rdgGvvS+b*t8BC5u3ru3 z^y<2+PVlGE2hp{ZCaFW8SM_`J3iHrVyd$-fo5yx*YKhvL6=Tj?LkbK7McM+-$6Ma( zA)=TGB1ycuoGOqkO!Y2CcRN0C!hpFG&72{0(*vr2+MiFc?dr^hGdXRrwA z&ZsT?@xyf4Xq@|xROHxU1;r$yw=FZFY}S)Zt}NEh-|$C)6Yf~U?#Ca&DyNl!4NXl% z>D1?2>q$!o%iz7+SD$#T!f!Tsk>IL>Bep^%y&nI<~D$D`oHJfNwV>fIXD2| zRSp2a0=D>TzIC=VGjnlf{Nus=YvRpJT(c`+LJGOiSaz@RG3XY|1P@U!S68Dw8?b(} z$7dx|XEwJjk!o0Wzeoi{t5%}1le}LXcb|je5!+ZnvbJPQ{S+0++C_67bVJi}rQW0Vu?NYmK+-xQOI*bAr`w9(MV*EpJ}_hAHQpT{T-pqn|+B@Ym={I@Nd-K2C|J`TdNJSD(SLuqGtt;`j8+cxBNa z6qYY+dNK$$^0e8~3W0o#l-{t2n4nPq0W-D^y@N`ZIEKNamUL^+@>1nsZX~H6=6FHa zp|1v$<1iel`9ZtSOGE`Oj^`kn02RAm zq0X}1Vw0C8UM$hfD!vvClQHlTB=81mgmm&I*x*wtB2*@%XtEfcmU&(SW?HwsZVvR$ z(sl*O8@2Ij${TPW`u6sRxn*Wt*ky4d4H%G*f{`s4Zm!crH=Kvuv*+6s`W;uLe_84L z8OUtzvybKTq^x#z#X003N*Hu=`}*_jZ0E=I)%DBmP0ri7pI;}(gZsr>=Zgp7+e2wv z*R$>E*hxv(o9@QC!G@oYPp1Ii?8B5ob;516)QR^e0e?dOA16zTB|`>N3ZJWmwi|X6 zX*y$xO1I0<)yfVV^X1Wl)Vu9}B=sK}#|}BVZKJ6jq4^T7W!!JcQjQIdqVbtF>< zlDo;Q90Q8C#>6km&pWCrH^w^Du~tR2okF2WgwzQ~z_s{a0uRry*u#7*5l%$CiA5VkKK4dL>u%Bw8n}ydMRq&sK68u>yq?4hZkU8sY}PodMZa&ehFLKoqRP#e7Z&5TOI!D{JENB>ZrvnVa;p_hJ93Z9k;%`O z_XB04+A1N7s#avm-l>+{WTM4Afw0r29nyBKJ>|Q_FfFAtn-uv+OdQPc`xN;jUP-IK zKURyc_aMKV4fV)%E*-%cHR8LG9Z7nH#)uq;N7AaQ5%!-ts&+Gu+({Xd6Eqqc$-mS_ zau?p<@KT&FXabNgBkd^!{?~1t+^QC=T@YvS&L-y|@hHlVptvIg7?H z*P^2e{-D@&_5Jqwq2>|yDE(i;P>p;p69R;EQ$1$H1kM&dUBVrT4SK)YZfoF}T}v-Y z`?gKb6|>Hh4{K^w4>gx2ud0%A{OT~ca<+9coeYswCbssn;{^A6=Dgt1Lc58BnoM)q z93&s8Mrwsgu3E=N>is ztupcE$Vwf3b~HVtbg9g!A0#>m8h6ai*K5Y0B7aG?v<{^*Sp&(YO&5~7)kY57wGzva ztE`n11D~2vCf-Fw3z4EOzB}0?ldl43pf=N7(fmTa7RsY4YPY6p-GmhmPw-GfGy>JY zwyc8IP4Y+*qo!NrA#|Cd4ai50H-#dbNKY8;CsQ7*B==C;oOh3+#JHmQ8i^W3E5o)Q z{5gwAdd=9+IL^rq+Ayg(DTKPcY>O^{axd9(wh{?mlRc0XTP9P>;PmGp8`oh?aIZ9l zs4|xRPY3?PNplMvS!1CDMJx(t6bS{W7Ud)$_QU zW+A47_6Q7<1s6NjA`Cr<2cKeVu19HFh+KsMlmX+=3EapL9D%nKSOGP@)hIo5B&pwUCwOX@AM;I?a%K=t}LWt1B3aP`Ht^ka~wLwW`5;?>nTy`(PC& zQNUI78wWQb2l>IJCu!Glu!b$Upz8mUM=4B(t@2dEWnJ=-fJ=9jLAr7UYe5_BWT$mL zo_i-jnb&siiJW_yZ3c(LB2nE%&VjIK+v(v(7#8XKmb~kx1J$%a2ZRC{1m`Dv7DROw zX;2P<6R}ZZP4*GpJLxe5b0rh8rM^nk1a4a#sYw57AV+-EBgsbXR4pD|-_9AnRIk*P zydj7%?Z+?lgQQQ%YkepgksZ=Dy(_j=)ZG;M%t_h{T#Co&Q?yZuo)MO096;RF;Dukf zO<~X+)Z%af%GJZ+rVXd9(snfWG%e7K2Cc;(Gr|W|A3M=ROEr814C9Na``=4s1Ju_-5-EyKPw+0B~0Vvg)Yqnam zjcj&=(oSPlz}^WZdZ~FfXJ(|6uI}>C(*-;z>DjCqL-)#QQXYxq&X$k%?G=^ZLvcLG zPt!U9Np>mC2EkZ)4^$ff_GotR;2oUdQ!?-&YU7;=kh^+cSu}^vfQq#HMl9)G`@B{D zR8InbEbo8Z`csTPrwby5zQJrgw*=;9b1_;~>pKyqvt2>zs$(0Hp2x2VdA`<3;Mv{&1tU~T5DP)%&qN#P8_RazlbQ7Oy>5$#c8*$NkH@j^<@H6>j;l>7m zW91{$s+SBtbe?F<@$L^@HmprETf6mrUVCoL!BnM;cd*F|)=-R;#(F-}t!6b!g$Nq| zxE{fd1%cv1@lnY~@k}*9*G3thgr2j)IrbErQynMS13v47LSD2MhuiHK?&NrZIQpX;y2sG zN8#VM#~HT)KI3OH7oBo-f49r*#ezBe074rpeq+5dJGCL*U#XqloLWkinMMuW7O|~J zInG_}utEJ?%wVho4-(QIh3941;a!B-*r?u-0~jz;W3YI>Ta(XPZmAN%3RQV@^^(jKes(ACY}DTd=U7y z#Y5M=))%1~&uyF}mr=8JXFMeo7aoWDm|z+@I0n1AA6B?v3Lw=rKH#j7AiTA0fLBer z*ti#OvG=uk>voy%oGdx_Xz5lyM+?&>}v6 z29(UH5X9>|MY>=g^z6!IR<@=>-OI!Y;Pdi_gZg`XHu%7mFdi6o3dQ-AM)_ka=l{z9)cCAj5RXTtVBFBoqMWs?6kOcTvSkU zt~+I_!lD#md%naOK`{7^+aWr#hNI7iv=fV@m3ub)?~CZQ`dA7)Z;z*+JH2jQuVy+@ zxg|LFg2`>0yz;iWIkD@ZrOa&~<{cERTM;m+(uhV;R^3uk+fv*FUJpr@k&$C+!R}By zgO=^c`h7}t_f4q8UqCZWZU+wHy9uBif@Y>%@Krf*-q?d%ida) zSwn(GzOjs6<+|X{y}N6eguZlZ)~x9HXs_&xcUg_T?{bY%7wm6?{A#|I04#Y!v(0r~ zGW?q6&Yrl`!OT8mtaON1>$6{`|NH@Q5X0yXp&!8Vybj^V1l1Gu>wCNXl~e(S*RQWW zH39aj+akrVGsw9WowqO*r z(cuFGL8(#59Q&PON-W`?fbl$WYRCIwy1@R7f?!9gs#%&KY5do*oRaKBiEjwv+O{LG zsh80tBSTv|iQ{4`KhY{$adw~TEhrIlvUy+bcBcyx!UPU%X;s8KXsObU?(|4rmw%s-b^5R zB#b08rctm6IA;}aqif{eEVGfhxpasdKaDPd#dk-|Bh^_Umi`2Iz;uq)FlNOTU(EFk z$`{SWmalQxXwKO!-99f|$)NtA|0kC?gN%!(8$@4ZjG z|H=`I^@%GYVVWcsM!5DcA~XA{2R1!;q%B1lRdH+ILu|MW8DjF0=FnZsN69HBGkO0& zuBN$`Y+?V#T!VKZwPC(F3?`^)Fiw{BEn!&Ss=)s!=bzP;!0*4u+W>Ln*Qqy)ErAnY^lR(Zm$oMEm(th|! zFa7ivUpm9_MM$^fcww+!mx^$e)#EDKT5)(2+B?q^a{}u#T*_?WhkAhAa(jE|?XacG z>C(l7f#24vn425rTW@v9L6Ju6kKg92nm@B%oPit(JkP%c!Ga^df(GMp%tbX=)st3|r>hd>MHlKhmS;dywoT;zL*D(*`+L#jWqlmRSHQNYt?vMO(>R z?9Q@J^^&ctk*$J)lkF&=>ETJ)dOY2ZAt?{APt=i>gR)%)tl7}ma3&s*KTwmDGz3tz zDu3(evTGAs3sb3OPH$SHK7HO|&K5a$XxS8VffzD3i8BX#wM{h3cA6I99mT5>rBn$@ zh05ek?6=i$^Us23Q^Lt_R{KB%^``AaADiTl%LUbnZOWUqUHh%CDoIkoqB{37OoXPq&!sEPLB2d1a!d7cw;R3le@L?sF{Xg-{df3%$)*#540XmOSQe^Q74Qi*;K_Q)7_YT z{S(>;%Th(*)ze(cZNV5Dsv2jwB~?%890x0&aZm%rD=o)!DX{HFYi3W|XA9k8U_mEP zdoa2x51`5^*WLERyh`fT<&nx=i==&A_wqL6YNqv&J`-euGJe^lNp!f0CDFV!I~Y#0 z=|*ayyk;li<%&vdSbIkA+6iG7pi@it>)M^`kS;xPKQ4;7SSa`H(mrzifM{rptfYHa zf07J>CpvBBca%TM!JxlAr?nEo95p7ZSCNSB=z67Gsl}k5nb&UMh!2|YEbs+h`%ayM zs_{5WXyn8-qevagMW%zu<`$ykoBdY^MBM|)fDU=S+d#j1G}XSE-FN6xT}w3V?b?+) z?A#Nb3!brzCb&meX_W%0X_Wnl!yuN;lBFof8GXF*Vy@`M$thqu;F#zh-XTMdeJ zNlBf6w(JTT?bwSgyY}d?s3jClzpNsn`*_0jlyg9BvNSn%Pocwv@OJowU}VXcVnOgN4zrUD7EiAEUbQTZTF zHqeZ-D}oXvZ_-=17?6n@KUH5@rR?W>6E<@TNM)IApLWmrm9(wN<~I=Nx7EuWquN(i zar!t6VNq;vX-!Kfut2(i(RtAby!VsMojmNZ*dTJ8qKoU#BWK!MDoBH{@20rN;|o;D z*?y|h6w^Q`B{L~og;(U?bv(`01zZ%&;^2_VNJoC4W@6RKW*dC>jT*`Xk`;?z&7$JR zC^G(3ODDAoIO*W*Py;y0j#CYXP4}yeH1VN8_3mb|QON_o07}wIi%Ex8X~bUpiq`AR zD}6!ZW`6^GcRdVY;%wnJH-=^9>*Q02D%f=7;L8UqZr?kVP6~a-%d7^-epnQWyl;(Q zln3>_6Zl5~nP)cD)%t@tFJ&C8isC`l!?PdpGM6;pFq|?V!(Z}OCnL|Oy~`$UOxiY~F&3$$LU@WV zvJq6+syE`|-SA+%+wTJSrETR3E%i6jli}2hvxw80=s#&bx^j!D+-}^R=S;kEBd#@e z>9kFgN36-Uue3S3G_G9SL=*ndFu?e{`Q`z7mI05BCc2W`BT_0}NBY=DCo^r#+TGL# zWyn}--<$OI9>IsB>lTbdd#kZcUvJQk0CLL@{j6$-SKsin!ISC_eI&TgRUm}+?0dmV z%zM_O-;`mb0?!a7Jya;#pAePq(f^5q^jzqr_mBX9133VI_7?{&jhxI(Ra~5`>@5DE zVml6yitlfIyI+Ma`|9{+oxZ!_wsMNq&)d~DoNO0@K!Aao;Y(#wN$-(c-_V2oKrs}k zEqNMpqvDFDUyetm^|efo4dZ>Fb-g?_8O?-6iZ}OJ^j;*q#oMf>%jLznpOlwC^gHKL zMkYW?cEq~p4>;xHdAZL3qfQF4Xu-QTF1jwMqP1Wy6uqy<<2(;elZ@yU5ZfKq1wfm^ zEf-Vk0Z z!SUy0LQ2B-AIpz-{m)m^hD=zVX>XWhqdG{z#tX-FZ^Vs~&v!?+via5}%uG4rAZh$l zH<+SvM^fU-vzNDs?Ts&2mg!M+g>bvKbLshC#0)Z*9BiVxbZC{0SrmfH|TtK0!|4@>Q*8A z7c_I`3!dtxAitvCtQa@`p2LbnHBZ+~p?A`F9=ll6JA`DaEQr{m@Q_mm{121R z^ZfI9+4VQKCt+T1%aJo-06IC&xoS;7~tsGEN4#g<2 zCg*_=J@-DTRHNHMTN+EVF22|kiVioh1ZGivf{75^(lpKbOyMdGPe@IJt1z009P_py zWM|)x^;uN3XJSXz3=>-=m?94lIV!FLqF;X)U^Y!l6_tNzLY9PeoV-R!9b_OFuoLqk z*tY=j0e%GY-09U^`RDv6Q`!T%aAue$q`)E~=9iS^iOBwyjfZ=GZL@M6pOmBkqN*7@ zL)uTMVp@V|-`3PdUB0Z65VOzmoqimH*SO#?z<+leaLf4PaIlQ=dm!CPsB-)d2J;#| z3Ey5_S!JyAqU+KKuP?nU$65<-42f_JsRP6ja{9Jt%URzRm^I1hB+L-|&gd}M*!0)< z=*RLkgK!<$HYdym^;XWK5r>PqRecXF7kOfS?z$mP*DZh7g6`AD=KZP}T$U`8- ziQcOXhsw32Lp%6VA!^7j#7RnZ3yGP=2}(}&sGeOBq}<83i-l^OIrAl9x0P6z|Ck8H z;NT22+ybR6#qc_uC2@#e#SMIsKPkeKi^c>yGKA+!kysOgN=jLa!jWUH@F?!M`A2U|{qvjI*-ZDm&JH;@P$u={_o?E;2X&x9wajZ7lxto^O{EfUcx|YmB)-KAeB@=@i zEGL+@zlnO8;^THaON}3+lFno~24qh`4xUkFv;NIxI063gQ~im)AAF5^;=R-H}tt0m!da8i=2UX_dauYy`)ODnUW_fmrBr* z8pIgl&FxtE%#x=F>gN#WQa-34o$!UxUz9yFK_=A}bQ|hyb1+&cWr;sNwM`a$;;^&j zzBXGEFQf`qlu)8q?{-bl;XO-*e#1a^6&H;yBJ-au33%UXW<<26`>eUvti^VMitX=n zf8KO;g4stoCenkQpzvA8>tKz+KF#FZt>uhR#z}|JHwDlN+c8(K_LQ)>vcE|nY!4gB zKru~EWg{0OUY&U?G$Q?I{-Gb>osnqyB((EkpmpZ92t!Lg*IIMK0=XussjF23FJnmG ziu=i_s$-hVrulBrurC_Jo-NvJjy{{^DhZzOjQ6YbW5?2v`eAE*QsuWs(T_TOd`XOo za#MZy>Rf8)mcsKPMAd|I+FZ|N#tkaHM^C1wAvnSA(wbb?wX`d^SeY?bxyBQH8}0Q- z2A0%)VIUva+?Tp|E$~iJOBPi)yAr3kKGCr zTnFN!09NZlWJl#%D0hd=JFi8vv&jIP?_ZI$SVXz)ZthItT_uAM#xOV(omt?P6biF% zAHR&1KhKN8UVh1%5JgcC1^?<$^wpKZZ&feH7Y#YSu83t)n1TQ>Fn4>FF?L~ycBQa* zh(Ib{n;H@N<(ZBg1HJ*qWFcTLZuChVx$B3vTUly3jwyFqrc75faj^nZK?V!zYw_;W z1p#;V)5T&{nG1-4?sSvkQ5v;rpX9nFkA>pihg^6%r}5$itO6PwaNHE4Bu49KJd$|P zOIUGnEA%^kYf=5!v;aIhueqlF8g>0XYc#tjlgrPjKF;JruXowqI%dH>{3E+-adj0k zzM)Sf?{TOH-4s`-QS&DE6t@H*2+_oXgo|bD2-=aowdZ3$5OD_Qnq1W{FuWug!v_Vr z)-Lzs!WWj_)7*zk$*Xvo--aR30@IZxC!7^lN2B0WWm z*vbOatbQ@eFhXIdLhw+be^BBzIWs2>zRxeNGGs#iCgTy4>$Y03cezgY+^FT&=VKPY zK0f1;tdB=_SDjXjNQ|FDpRRo`Cus<$S!@j+FD#4Jpm)z6v+4I;>CFD1anJ1#BfNR+ zWzEKvBxY}N@!T8bVEMH+g4?S0)ulJ-(Dx$?^ZF5VWJ`+G=L95dyb=L~(INZuoYygs zQA$j=?>Rub`R`qzz|f8@2hg%osQ>`VKP}4v=z6-CIjNYrxcu@#8GrlD%wTL~cbdv4 z115wNx-mg69xW_MY)srkfdr>dqzz4+kKu<*M0e>Q&$&nNSv(x3QB8B zM@LWpPU!`a+u1veE8%vQK}YSsNazlIdLR|a`^+nd3?sDb!~a;JthGJ~@j9UgexKp{qr> z0@8lWGk}&0C{AnWt(h>6=X6ZT?^YAu@GLx(&RaWZaqOx*?D~RXMd1Nm@8rC*l_p1E+qzlNAjARcCAKj)6i=mAV`B9S-H*W65p?1%4gw zl}G!Q_*}V0d)KkOKfVtA3ooS~3dfs;{ycp|!gqK0mov(BTM^PHn1J`t9`up&oxxok zP^0ThUZ$(KP^L6R6HZaO92%`**#wcK6C9D>2Ia(v+F_z z{hUTW%c*=e=?takHd&=y>jxR7$`hoFQ6e+Df&wAlBeU}-usJ)Kwp2x0-8Vj6&8d7>HwQ`cM)Iy^Q?wmpeP5FSOM)18CZg5YHy-|5cs=Nf{ikB-6))7P9f~O=QG7qj-Q>WF;V`77{WLeV{3I)wSn6 zyi~*aM#fj2wbV_MbFL?+N%E!bw$dO*CiSm05@eA|fXF7{G8&1gJ(|pdZti06a-DZv z%RypM+dpV_(y!;c3#{0&D0?8X3+eW_C3p_l1{SaJ*~Z1!^V*wj<@z!O(qTRf3TVFL zjM%}p>t^o3>*HgZMMcJs4OEc)X>|-j#2Jc%qc~}dg8XvgYKZfE5F*6o_DWUeH^@~gx7QaAGbw*f8o;8(P7cIh%ls82^&S5yDc%qv@)3t+2o>CVndobuynXAE9bYwU2WNcZ} zF0b=l{%&1}+z9C+k#FH-&^v`>O>EIq5fXxNTTxR=FTjEP@A;2mx#4RS&;~_-kuaox zw?QKZhyR%%aLE3;G86uACqgg4?h!)IqciQOLIu^83UR$GYXOel7N*?D6f&XxI3xPz zDIHP6KjMH9C%%&Iaf2Mybe0yw(+}OWGDhc%3#FkjdpmX1B(!}?E#rV)VEB;=i|F`NmFn#)4#s2Tg|5>B_w<-Vt4fBWc|EFU4cbwk~SN}%h1(vh^ z|H@c@NBKSH`!|XN_8%y}XMleP_&wtOHvkOL9{_*H-+x#AJy7_!Y7B5c@GErxd)V-I zgx|Lie0%*p!?7x2fKg03u*#H0l literal 0 HcmV?d00001 diff --git a/styles/GIS_63000F_Objekt_Denkmalschutz.qml b/styles/GIS_63000F_Objekt_Denkmalschutz.qml new file mode 100644 index 0000000..06bb9e5 --- /dev/null +++ b/styles/GIS_63000F_Objekt_Denkmalschutz.qml @@ -0,0 +1,609 @@ + + + + 1 + 1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "gml_id" + + + + + + 0 + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + 0 + generatedlayout + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "gml_id" + + 2 + diff --git a/styles/GIS_Flst_Beschriftung_ALKIS_NAS.qml b/styles/GIS_Flst_Beschriftung_ALKIS_NAS.qml new file mode 100644 index 0000000..5e40734 --- /dev/null +++ b/styles/GIS_Flst_Beschriftung_ALKIS_NAS.qml @@ -0,0 +1,349 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 2 + diff --git a/styles/GIS_LfULG_LSG.qml b/styles/GIS_LfULG_LSG.qml new file mode 100644 index 0000000..28082ba --- /dev/null +++ b/styles/GIS_LfULG_LSG.qml @@ -0,0 +1,371 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 2 + -- 2.49.1 From 5177a526a3d3727e148acced3cdbdeeae94241b3 Mon Sep 17 00:00:00 2001 From: daniel Date: Tue, 2 Dec 2025 20:56:23 +0100 Subject: [PATCH 02/10] stile in sn_basis verschoben --- styles/GIS_63000F_Objekt_Denkmalschutz.qml | 609 --------------------- styles/GIS_Flst_Beschriftung_ALKIS_NAS.qml | 349 ------------ styles/GIS_LfULG_LSG.qml | 371 ------------- 3 files changed, 1329 deletions(-) delete mode 100644 styles/GIS_63000F_Objekt_Denkmalschutz.qml delete mode 100644 styles/GIS_Flst_Beschriftung_ALKIS_NAS.qml delete mode 100644 styles/GIS_LfULG_LSG.qml diff --git a/styles/GIS_63000F_Objekt_Denkmalschutz.qml b/styles/GIS_63000F_Objekt_Denkmalschutz.qml deleted file mode 100644 index 06bb9e5..0000000 --- a/styles/GIS_63000F_Objekt_Denkmalschutz.qml +++ /dev/null @@ -1,609 +0,0 @@ - - - - 1 - 1 - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "gml_id" - - - - - - 0 - 0 - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0 - - - 0 - generatedlayout - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "gml_id" - - 2 - diff --git a/styles/GIS_Flst_Beschriftung_ALKIS_NAS.qml b/styles/GIS_Flst_Beschriftung_ALKIS_NAS.qml deleted file mode 100644 index 5e40734..0000000 --- a/styles/GIS_Flst_Beschriftung_ALKIS_NAS.qml +++ /dev/null @@ -1,349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0 - 0 - 2 - diff --git a/styles/GIS_LfULG_LSG.qml b/styles/GIS_LfULG_LSG.qml deleted file mode 100644 index 28082ba..0000000 --- a/styles/GIS_LfULG_LSG.qml +++ /dev/null @@ -1,371 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0 - 0 - 2 - -- 2.49.1 From f5a5ed167bff4d84aa287ca4de2f245e1c2a593e Mon Sep 17 00:00:00 2001 From: daniel Date: Fri, 5 Dec 2025 13:07:37 +0100 Subject: [PATCH 03/10] =?UTF-8?q?Dateieingabe=20Verfahrens-DB=20und=20Link?= =?UTF-8?q?liste=20in=20Tab=20A=20eingef=C3=BCgt,=20Verfahrensgebiets-Laye?= =?UTF-8?q?rauswahl=20in=20Tab=20A=20eingef=C3=BCgt.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/__init__.py | 1 + test/run_tests.py | 27 +++ test/start_osgeo4w_qgis.bat | 9 + test/test_tab_a.py | 112 +++++++++++++ ui/tabs/tab_a.py | 318 +++++++++++++++++++++++++++++++++++- 5 files changed, 460 insertions(+), 7 deletions(-) create mode 100644 test/__init__.py create mode 100644 test/run_tests.py create mode 100644 test/start_osgeo4w_qgis.bat create mode 100644 test/test_tab_a.py diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..324c4b2 --- /dev/null +++ b/test/__init__.py @@ -0,0 +1 @@ +#Testordner \ No newline at end of file diff --git a/test/run_tests.py b/test/run_tests.py new file mode 100644 index 0000000..f61f42f --- /dev/null +++ b/test/run_tests.py @@ -0,0 +1,27 @@ +import unittest +import os +import sys + +# Plugin-Hauptverzeichnis ermitteln +BASE_DIR = os.path.abspath(os.path.dirname(__file__)) + +# Plugin-Ordner in den Python-Pfad aufnehmen +sys.path.insert(0, BASE_DIR) + +def run(): + # Testverzeichnis + test_dir = os.path.join(BASE_DIR, "tests") + + # Test-Suite automatisch finden + suite = unittest.defaultTestLoader.discover(test_dir) + + # Runner starten + runner = unittest.TextTestRunner(verbosity=2) + result = runner.run(suite) + + # Exit-Code setzen (für CI oder Skripte nützlich) + sys.exit(not result.wasSuccessful()) + + +if __name__ == "__main__": + run() diff --git a/test/start_osgeo4w_qgis.bat b/test/start_osgeo4w_qgis.bat new file mode 100644 index 0000000..a4b0c23 --- /dev/null +++ b/test/start_osgeo4w_qgis.bat @@ -0,0 +1,9 @@ +@echo off +SET OSGEO4W_ROOT=D:\QGISQT5 +call %OSGEO4W_ROOT%\bin\o4w_env.bat +set QGIS_PREFIX_PATH=%OSGEO4W_ROOT%\apps\qgis +set PYTHONPATH=%QGIS_PREFIX_PATH%\python;%PYTHONPATH% +set PATH=%OSGEO4W_ROOT%\bin;%QGIS_PREFIX_PATH%\bin;%PATH% + +REM Neue Eingabeaufforderung starten und Python-Skript ausführen +start cmd /k "python run_tests.py" diff --git a/test/test_tab_a.py b/test/test_tab_a.py new file mode 100644 index 0000000..a85fbc2 --- /dev/null +++ b/test/test_tab_a.py @@ -0,0 +1,112 @@ +import unittest +import os +import tempfile +import sys + +# Plugin-Ordner in den Python-Pfad aufnehmen +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from qgis.PyQt.QtWidgets import QApplication +from qgis.core import QgsProject, QgsVectorLayer + +from sn_plan41.ui.tabs import tab_a + + +class TestTabA(unittest.TestCase): + + @classmethod + def setUpClass(cls): + """Qt-Anwendung initialisieren.""" + cls.app = QApplication([]) + + def setUp(self): + """Vor jedem Test: Projektvariablen löschen und TabA neu erzeugen.""" + self.project = QgsProject.instance() + self.project.setCustomVariables({}) + + # TabA erzeugen + self.tab = TabA() + + # Temporäre Testdateien + self.tmp_dir = tempfile.gettempdir() + self.test_db = os.path.join(self.tmp_dir, "test_db.gpkg") + self.test_link = os.path.join(self.tmp_dir, "test_link.gpkg") + + # Dummy-Dateien anlegen + with open(self.test_db, "w") as f: + f.write("") + with open(self.test_link, "w") as f: + f.write("") + + # --------------------------------------------------------- + # Verfahrens-DB speichern & wiederherstellen + # --------------------------------------------------------- + def test_save_and_restore_verfahrens_db(self): + self.tab.on_file_changed(self.test_db) + + vars = self.project.customVariables() + self.assertEqual(vars.get("sn_verfahrens_db"), self.test_db) + + tab2 = TabA() + self.assertEqual(tab2.verfahrens_db, self.test_db) + self.assertEqual(tab2.file_widget.filePath(), self.test_db) + + # --------------------------------------------------------- + # Verfahrens-DB löschen + # --------------------------------------------------------- + def test_delete_verfahrens_db(self): + self.tab.on_file_changed(self.test_db) + self.tab.on_file_changed("") + + vars = self.project.customVariables() + self.assertNotIn("sn_verfahrens_db", vars) + self.assertIsNone(self.tab.verfahrens_db) + + # --------------------------------------------------------- + # Linkliste speichern & löschen + # --------------------------------------------------------- + def test_save_and_delete_linkliste(self): + self.tab.on_linkliste_changed(self.test_link) + + vars = self.project.customVariables() + self.assertEqual(vars.get("sn_linkliste"), self.test_link) + + self.tab.on_linkliste_changed("") + + vars = self.project.customVariables() + self.assertNotIn("sn_linkliste", vars) + + # --------------------------------------------------------- + # Layer-Vorauswahl + # --------------------------------------------------------- + def test_preselect_verfahrensgebiet_layer(self): + vg_layer = QgsVectorLayer("Polygon?crs=EPSG:4326", "Verfahrensgebiet", "memory") + other_layer = QgsVectorLayer("Polygon?crs=EPSG:4326", "AndereDaten", "memory") + + self.project.addMapLayer(other_layer) + self.project.addMapLayer(vg_layer) + + tab2 = TabA() + + selected = tab2.layer_combo.currentLayer() + self.assertIsNotNone(selected) + self.assertEqual(selected.name(), "Verfahrensgebiet") + + # --------------------------------------------------------- + # Gespeicherter Layer wird wiederhergestellt + # --------------------------------------------------------- + def test_restore_saved_layer(self): + vg_layer = QgsVectorLayer("Polygon?crs=EPSG:4326", "Verfahrensgebiet", "memory") + self.project.addMapLayer(vg_layer) + + vars = {"sn_verfahrensgebiet_layer": vg_layer.id()} + self.project.setCustomVariables(vars) + + tab2 = TabA() + + selected = tab2.layer_combo.currentLayer() + self.assertEqual(selected.id(), vg_layer.id()) + + +if __name__ == "__main__": + unittest.main() diff --git a/ui/tabs/tab_a.py b/ui/tabs/tab_a.py index db0f486..194c221 100644 --- a/ui/tabs/tab_a.py +++ b/ui/tabs/tab_a.py @@ -1,12 +1,316 @@ -from qgis.PyQt.QtWidgets import QWidget, QVBoxLayout, QLabel, QLineEdit +import os + +from qgis.PyQt.QtWidgets import ( + QWidget, QVBoxLayout, QLabel, QMessageBox, QPushButton, + QFileDialog, QToolButton, QSizePolicy +) +from qgis.PyQt.QtCore import Qt +from qgis.gui import QgsFileWidget, QgsMapLayerComboBox +from qgis.core import QgsProject, QgsMapLayerProxyModel + class TabA(QWidget): - tab_title = "Tab A" + tab_title = "Daten" def __init__(self, parent=None): super().__init__(parent) - layout = QVBoxLayout() - layout.addWidget(QLabel("Plugin2 – Tab A")) - layout.addWidget(QLineEdit("Feld A1")) - layout.addWidget(QLineEdit("Feld A2")) - self.setLayout(layout) + + # Variablen initialisieren + self.verfahrens_db = None + self.lokale_linkliste = None + + # --------------------------------------------------------- + # Hauptlayout + # --------------------------------------------------------- + main_layout = QVBoxLayout() + main_layout.setSpacing(4) + main_layout.setContentsMargins(4, 4, 4, 4) + + # --------------------------------------------------------- + # COLLAPSIBLE GRUPPE: Verfahrens-Datenbank + # --------------------------------------------------------- + self.group_button = QToolButton() + self.group_button.setText("Verfahrens-Datenbank") + self.group_button.setCheckable(True) + self.group_button.setChecked(True) + self.group_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) + self.group_button.setArrowType(Qt.DownArrow) + self.group_button.setStyleSheet("font-weight: bold;") + self.group_button.toggled.connect(self.toggle_group) + main_layout.addWidget(self.group_button, 0) + + # Inhalt der Gruppe + self.group_content = QWidget() + self.group_content.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) + + group_layout = QVBoxLayout() + group_layout.setSpacing(2) + group_layout.setContentsMargins(10, 4, 4, 4) + + # Hinweis + hinweis1 = QLabel("bestehende Datei auswählen") + group_layout.addWidget(hinweis1) + + # Datei-Auswahl + self.file_widget = QgsFileWidget() + self.file_widget.setStorageMode(QgsFileWidget.GetFile) + self.file_widget.setFilter("Geopackage (*.gpkg)") + self.file_widget.fileChanged.connect(self.on_file_changed) + group_layout.addWidget(self.file_widget) + + # Hinweis "-oder-" + hinweis2 = QLabel("-oder-") + group_layout.addWidget(hinweis2) + + # Button: Neue Datei + self.btn_new = QPushButton("Neue Verfahrens-DB anlegen") + self.btn_new.clicked.connect(self.create_new_gpkg) + group_layout.addWidget(self.btn_new) + + self.group_content.setLayout(group_layout) + main_layout.addWidget(self.group_content, 0) + + # --------------------------------------------------------- + # COLLAPSIBLE Optional-Bereich + # --------------------------------------------------------- + self.optional_button = QToolButton() + self.optional_button.setText("Optional: Lokale Linkliste") + self.optional_button.setCheckable(True) + self.optional_button.setChecked(False) + self.optional_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) + self.optional_button.setArrowType(Qt.RightArrow) + self.optional_button.setStyleSheet("font-weight: bold; margin-top: 6px;") + self.optional_button.toggled.connect(self.toggle_optional) + main_layout.addWidget(self.optional_button, 0) + + # Inhalt optional + self.optional_content = QWidget() + self.optional_content.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) + + optional_layout = QVBoxLayout() + optional_layout.setSpacing(2) + optional_layout.setContentsMargins(10, 4, 4, 20) + + # Hinweistext + optional_hint = QLabel("(frei lassen für globale Linkliste)") + optional_layout.addWidget(optional_hint) + + # Datei-Auswahl für Linkliste + self.linkliste_widget = QgsFileWidget() + self.linkliste_widget.setStorageMode(QgsFileWidget.GetFile) + self.linkliste_widget.setFilter("Geopackage (*.gpkg)") + self.linkliste_widget.fileChanged.connect(self.on_linkliste_changed) + optional_layout.addWidget(self.linkliste_widget) + main_layout.addWidget(self.optional_content, 0) + + # --------------------------------------------------------- + # Layer-Auswahlfeld + # --------------------------------------------------------- + layer_label = QLabel("Verfahrensgebiet-Layer auswählen") + layer_label.setStyleSheet("font-weight: bold; margin-top: 6px;") + main_layout.addWidget(layer_label) + + self.layer_combo = QgsMapLayerComboBox() + self.layer_combo.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) + + # ✅ QGIS 3.22–3.46 kompatibel + self.layer_combo.setFilters(QgsMapLayerProxyModel.VectorLayer) + + # Layerwechsel speichern + self.layer_combo.layerChanged.connect(self.on_layer_changed) + + main_layout.addWidget(self.layer_combo) + + self.optional_content.setLayout(optional_layout) + self.optional_content.setVisible(False) + + + # Spacer + main_layout.addStretch(1) + + self.setLayout(main_layout) + + # ✅ gespeicherte Werte wiederherstellen (jetzt existieren die Widgets!) + self.restore_saved_values() + + # ✅ Layer-Vorauswahl durchführen + self.preselect_verfahrensgebiet_layer() + + # --------------------------------------------------------- + # Collapsible Gruppe ein-/ausblenden + # --------------------------------------------------------- + def toggle_group(self, checked): + self.group_button.setArrowType(Qt.DownArrow if checked else Qt.RightArrow) + self.group_content.setVisible(checked) + + def toggle_optional(self, checked): + self.optional_button.setArrowType(Qt.DownArrow if checked else Qt.RightArrow) + self.optional_content.setVisible(checked) + + # --------------------------------------------------------- + # Datei-Auswahl: Verfahrens-DB + # --------------------------------------------------------- + def on_file_changed(self, path: str): + if not path: + self.verfahrens_db = None + + # ✅ Projektvariable löschen + vars = QgsProject.instance().customVariables() + if "sn_verfahrens_db" in vars: + del vars["sn_verfahrens_db"] + QgsProject.instance().setCustomVariables(vars) + + self.update_group_button_color() + return + + if not path.lower().endswith(".gpkg"): + path += ".gpkg" + self.file_widget.setFilePath(path) + + if os.path.exists(path): + self.verfahrens_db = path + else: + self.verfahrens_db = None + QMessageBox.warning(self, "Datei nicht gefunden", f"Die Datei existiert nicht:\n{path}") + self.file_widget.setFilePath("") + + # ✅ speichern + vars = QgsProject.instance().customVariables() + vars["sn_verfahrens_db"] = self.verfahrens_db + QgsProject.instance().setCustomVariables(vars) + + self.update_group_button_color() + + # --------------------------------------------------------- + # Datei-Auswahl: Lokale Linkliste + # --------------------------------------------------------- + def on_linkliste_changed(self, path: str): + if not path: + self.lokale_linkliste = None + + vars = QgsProject.instance().customVariables() + if "sn_linkliste" in vars: + del vars["sn_linkliste"] + QgsProject.instance().setCustomVariables(vars) + + return + + + if not path.lower().endswith(".gpkg"): + path += ".gpkg" + self.linkliste_widget.setFilePath(path) + + if os.path.exists(path): + self.lokale_linkliste = path + else: + self.lokale_linkliste = None + QMessageBox.warning(self, "Datei nicht gefunden", f"Die Datei existiert nicht:\n{path}") + self.linkliste_widget.setFilePath("") + + # ✅ speichern + vars = QgsProject.instance().customVariables() + vars["sn_linkliste"] = self.lokale_linkliste + QgsProject.instance().setCustomVariables(vars) + + # --------------------------------------------------------- + # Layer-Auswahl speichern + # --------------------------------------------------------- + def on_layer_changed(self, layer): + if layer: + vars = QgsProject.instance().customVariables() + vars["sn_verfahrensgebiet_layer"] = layer.id() + QgsProject.instance().setCustomVariables(vars) + + # --------------------------------------------------------- + # Button-Farbe aktualisieren + # --------------------------------------------------------- + def update_group_button_color(self): + if self.verfahrens_db: + self.group_button.setStyleSheet("font-weight: bold; background-color: #c4f7c4;") + else: + self.group_button.setStyleSheet("font-weight: bold;") + + # --------------------------------------------------------- + # Vorauswahl des Layers "Verfahrensgebiet" + # --------------------------------------------------------- + def preselect_verfahrensgebiet_layer(self): + project = QgsProject.instance() + + # ✅ zuerst gespeicherten Layer wiederherstellen + saved_layer_id = project.customVariables().get("sn_verfahrensgebiet_layer", None) + if saved_layer_id: + layer = project.mapLayer(saved_layer_id) + if layer: + self.layer_combo.setLayer(layer) + return + + # ✅ sonst nach Namen suchen + for layer in project.mapLayers().values(): + if "verfahrensgebiet" in layer.name().lower(): + self.layer_combo.setLayer(layer) + return + + # ✅ Fallback: erster Layer + if self.layer_combo.count() > 0: + self.layer_combo.setCurrentIndex(0) + + # --------------------------------------------------------- + # Werte wiederherstellen + # --------------------------------------------------------- + def restore_saved_values(self): + project = QgsProject.instance() + vars = project.customVariables() + + # ✅ Verfahrens-DB + saved_db = vars.get("sn_verfahrens_db", None) + if saved_db and os.path.exists(saved_db): + self.verfahrens_db = saved_db + self.file_widget.setFilePath(saved_db) + self.update_group_button_color() + + # ✅ Linkliste + saved_link = vars.get("sn_linkliste", None) + if saved_link and os.path.exists(saved_link): + self.lokale_linkliste = saved_link + self.linkliste_widget.setFilePath(saved_link) + def create_new_gpkg(self): + """Öffnet einen Save-Dialog und legt eine neue GPKG-Datei an.""" + file_path, _ = QFileDialog.getSaveFileName( + self, + "Neue Verfahrens-Datenbank anlegen", + "", + "Geopackage (*.gpkg);;Alle Dateien (*)" + ) + + if not file_path: + return # Abbruch + + # Automatisch .gpkg anhängen + if not file_path.lower().endswith(".gpkg"): + file_path += ".gpkg" + + # Existiert Datei bereits? + if os.path.exists(file_path): + overwrite = QMessageBox.question( + self, + "Datei existiert bereits", + f"Die Datei existiert bereits:\n\n{file_path}\n\nSoll sie überschrieben werden?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + if overwrite != QMessageBox.Yes: + return + + # Datei anlegen + try: + open(file_path, "w").close() + except Exception as e: + QMessageBox.critical(self, "Fehler", f"Die Datei konnte nicht angelegt werden:\n{e}") + return + + # Datei übernehmen + self.verfahrens_db = file_path + self.file_widget.setFilePath(file_path) + self.update_group_button_color() + + QMessageBox.information(self, "Projekt-DB angelegt", f"Neue Projekt-Datenbank wurde angelegt:\n{file_path}") -- 2.49.1 From 76b8a8ad133871ca9b86241441f89e58da21f49a Mon Sep 17 00:00:00 2001 From: daniel Date: Thu, 11 Dec 2025 21:43:00 +0100 Subject: [PATCH 04/10] Dateiendung Linkliste korrigiert, Datenbank-ERD angelegt --- doc/Datenbank_ERD.md | 37 +++++++++++++++++++++++++++++++++++++ ui/tabs/tab_a.py | 4 ++-- 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 doc/Datenbank_ERD.md diff --git a/doc/Datenbank_ERD.md b/doc/Datenbank_ERD.md new file mode 100644 index 0000000..4a1b9ad --- /dev/null +++ b/doc/Datenbank_ERD.md @@ -0,0 +1,37 @@ +```mermaid +erDiagram +tbl_akteure{ + Int4 ID PK + varchar Bezeichnung + varchar(6) vkz "Verfahren, für das der Akteur relevant ist" +} +tbl_konten{ + int4 ID PK + varchar(3) kontonr + varchar bezeichnung +} +tbl_ausbauart{ + int4 id PK + varchar Ausbauart_text + int4 preis + varchar(3) tbe_nr + varchar(3) Ausbauart_nr "Nr. der Ausbauart zur einfacheren Referenz" +} +p41_Massnahmen_linie{ + int4 id PK + varchar mnnr "Berechnet aus mn_konto und lfd_nr" + varchar mnname + varchar ausbauart FK "ref:tbl_ausbauart" + varchar(3) mn_konto FK "ref: tbl_konten.id" + varchar(2) lfd_nr + varchar(1) tbe + bool umsetzung + varchar unterhalt_bisher FK "ref: tbl_akteure.id" + varchar unterhalt_zukuenftig FK "ref: tbl_akteure.id" + varchar bautraeger FK "ref: tbl_akteure.id" + varchar kostentraeger FK "ref: tbl_akteure.id" +} + +tbl_konten ||--o{ p41_Massnahmen_linie : verwendet +tbl_akteure ||--o{ p41_Massnahmen_linie : verwendet +tbl_ausbauart ||--o{ p41_Massnahmen_linie : verwendet \ No newline at end of file diff --git a/ui/tabs/tab_a.py b/ui/tabs/tab_a.py index 194c221..a445c4a 100644 --- a/ui/tabs/tab_a.py +++ b/ui/tabs/tab_a.py @@ -196,8 +196,8 @@ class TabA(QWidget): return - if not path.lower().endswith(".gpkg"): - path += ".gpkg" + if not path.lower().endswith(".xlsx"): + path += ".xlsx" self.linkliste_widget.setFilePath(path) if os.path.exists(path): -- 2.49.1 From c8409c7f25c7c46d95098fe16201386d7b32251b Mon Sep 17 00:00:00 2001 From: daniel Date: Mon, 15 Dec 2025 16:13:26 +0100 Subject: [PATCH 05/10] =?UTF-8?q?fehlende=20Attribute=20in=20der=20ERD.md?= =?UTF-8?q?=20erg=C3=A4nzt,=20Datentypen=20korrigiert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/Datenbank_ERD.md | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/doc/Datenbank_ERD.md b/doc/Datenbank_ERD.md index 4a1b9ad..1ac25b3 100644 --- a/doc/Datenbank_ERD.md +++ b/doc/Datenbank_ERD.md @@ -1,12 +1,11 @@ ```mermaid erDiagram tbl_akteure{ - Int4 ID PK + Int4 fid PK varchar Bezeichnung varchar(6) vkz "Verfahren, für das der Akteur relevant ist" } tbl_konten{ - int4 ID PK varchar(3) kontonr varchar bezeichnung } @@ -21,17 +20,34 @@ p41_Massnahmen_linie{ int4 id PK varchar mnnr "Berechnet aus mn_konto und lfd_nr" varchar mnname - varchar ausbauart FK "ref:tbl_ausbauart" - varchar(3) mn_konto FK "ref: tbl_konten.id" + int4 ausbauart FK "ref:tbl_ausbauart.id" + varchar(3) mn_konto FK "ref: tbl_konten.kontonr" varchar(2) lfd_nr varchar(1) tbe bool umsetzung - varchar unterhalt_bisher FK "ref: tbl_akteure.id" - varchar unterhalt_zukuenftig FK "ref: tbl_akteure.id" - varchar bautraeger FK "ref: tbl_akteure.id" - varchar kostentraeger FK "ref: tbl_akteure.id" + int4 unterhalt_bisher FK "ref: tbl_akteure.id" + int4 unterhalt_zukuenftig FK "ref: tbl_akteure.id" + int4 bautraeger FK "ref: tbl_akteure.id" + int4 kostentraeger FK "ref: tbl_akteure.id" + date Planungsjahr + date baujahr + date fertigstellungspflege + date entwicklungspflege_1 + date entwicklungspflege_2 + float8 fahrbahnbreite + float8 gesamtbreite + varchar vorgesehene_regelungen + varchar bemerkungen + int4 laenge + int4 flaeche + varchar foerdersatz + bool ingenieurbauwerk + varchar bildpfad + bool fertiggestellt + int4 ausbauart_nr } tbl_konten ||--o{ p41_Massnahmen_linie : verwendet tbl_akteure ||--o{ p41_Massnahmen_linie : verwendet -tbl_ausbauart ||--o{ p41_Massnahmen_linie : verwendet \ No newline at end of file +tbl_ausbauart ||--o{ p41_Massnahmen_linie : verwendet +``` \ No newline at end of file -- 2.49.1 From 8f8a1ccde3f9a5f3d3dd8a597783755e2bec0e48 Mon Sep 17 00:00:00 2001 From: daniel Date: Wed, 17 Dec 2025 11:41:41 +0100 Subject: [PATCH 06/10] =?UTF-8?q?Anpassung=20ERD=20f=C3=BCr=20Andreas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/Datenbank_ERD.md | 15 ++++++++++----- test/test_tab_a.py | 2 +- ui/tabs/tab_a.py | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/doc/Datenbank_ERD.md b/doc/Datenbank_ERD.md index 1ac25b3..e53d924 100644 --- a/doc/Datenbank_ERD.md +++ b/doc/Datenbank_ERD.md @@ -16,8 +16,13 @@ tbl_ausbauart{ varchar(3) tbe_nr varchar(3) Ausbauart_nr "Nr. der Ausbauart zur einfacheren Referenz" } +tbl_Massnahme{ + int4 MnNr + int4 Abschnitte FK +} p41_Massnahmen_linie{ int4 id PK + geom Geometrie varchar mnnr "Berechnet aus mn_konto und lfd_nr" varchar mnname int4 ausbauart FK "ref:tbl_ausbauart.id" @@ -29,11 +34,10 @@ p41_Massnahmen_linie{ int4 unterhalt_zukuenftig FK "ref: tbl_akteure.id" int4 bautraeger FK "ref: tbl_akteure.id" int4 kostentraeger FK "ref: tbl_akteure.id" - date Planungsjahr - date baujahr - date fertigstellungspflege - date entwicklungspflege_1 - date entwicklungspflege_2 + int4 Planungsjahr + int4 baujahr + int4 Pflege_Anfang + int4 Pflege_Ende float8 fahrbahnbreite float8 gesamtbreite varchar vorgesehene_regelungen @@ -45,6 +49,7 @@ p41_Massnahmen_linie{ varchar bildpfad bool fertiggestellt int4 ausbauart_nr + bool plangenehmigt } tbl_konten ||--o{ p41_Massnahmen_linie : verwendet diff --git a/test/test_tab_a.py b/test/test_tab_a.py index a85fbc2..4524392 100644 --- a/test/test_tab_a.py +++ b/test/test_tab_a.py @@ -30,7 +30,7 @@ class TestTabA(unittest.TestCase): # Temporäre Testdateien self.tmp_dir = tempfile.gettempdir() self.test_db = os.path.join(self.tmp_dir, "test_db.gpkg") - self.test_link = os.path.join(self.tmp_dir, "test_link.gpkg") + self.test_link = os.path.join(self.tmp_dir, "test_link.xlsx") # Dummy-Dateien anlegen with open(self.test_db, "w") as f: diff --git a/ui/tabs/tab_a.py b/ui/tabs/tab_a.py index a445c4a..7480955 100644 --- a/ui/tabs/tab_a.py +++ b/ui/tabs/tab_a.py @@ -98,7 +98,7 @@ class TabA(QWidget): # Datei-Auswahl für Linkliste self.linkliste_widget = QgsFileWidget() self.linkliste_widget.setStorageMode(QgsFileWidget.GetFile) - self.linkliste_widget.setFilter("Geopackage (*.gpkg)") + self.linkliste_widget.setFilter("Excelliste (*.xlsx)") self.linkliste_widget.fileChanged.connect(self.on_linkliste_changed) optional_layout.addWidget(self.linkliste_widget) main_layout.addWidget(self.optional_content, 0) -- 2.49.1 From 3bfd88b51e2003c728bc72ae572b3f2f0aadaa49 Mon Sep 17 00:00:00 2001 From: daniel Date: Thu, 8 Jan 2026 17:13:43 +0100 Subject: [PATCH 07/10] =?UTF-8?q?auf=20Wrapper=20umgestellt,=20tests=20erg?= =?UTF-8?q?=C3=A4nzt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 33 ++- pyrightconfig.json | 3 + test/run_tests.py | 27 --- test/test_tab_a.py | 112 --------- {test => tests}/__init__.py | 0 tests/run_tests.py | 148 ++++++++++++ {test => tests}/start_osgeo4w_qgis.bat | 0 tests/test_tab_a_logic.py | 153 ++++++++++++ tests/test_tab_a_ui.py | 57 +++++ ui/dockwidget.py | 10 +- ui/tab_a_logic.py | 132 +++++++++++ ui/tab_a_ui.py | 308 ++++++++++++++++++++++++ ui/tabs/tab_a.py | 316 ------------------------- 13 files changed, 826 insertions(+), 473 deletions(-) create mode 100644 pyrightconfig.json delete mode 100644 test/run_tests.py delete mode 100644 test/test_tab_a.py rename {test => tests}/__init__.py (100%) create mode 100644 tests/run_tests.py rename {test => tests}/start_osgeo4w_qgis.bat (100%) create mode 100644 tests/test_tab_a_logic.py create mode 100644 tests/test_tab_a_ui.py create mode 100644 ui/tab_a_logic.py create mode 100644 ui/tab_a_ui.py delete mode 100644 ui/tabs/tab_a.py diff --git a/main.py b/main.py index d77f966..66f2b59 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,8 @@ +# sn_plan41/main.py + from qgis.utils import plugins from sn_basis.ui.dockmanager import DockManager -from .ui.dockwidget import DockWidget +from sn_plan41.ui.dockwidget import DockWidget class Plan41: @@ -15,14 +17,17 @@ class Plan41: def initGui(self): basis = plugins.get("sn_basis") - if basis and basis.ui: - self.action = basis.ui.add_action( - self.plugin_name, - self.run, - tooltip=f"Öffnet {self.plugin_name}", - priority=20 - ) - basis.ui.finalize_menu_and_toolbar() + if not basis or not getattr(basis, "ui", None): + return + + self.action = basis.ui.add_action( + self.plugin_name, + self.run, + tooltip=f"Öffnet {self.plugin_name}", + priority=20, + ) + basis.ui.finalize_menu_and_toolbar() + print("Plan41/sn_Basis:initGui called") def unload(self): if self.dockwidget: @@ -32,13 +37,15 @@ class Plan41: if self.action: basis = plugins.get("sn_basis") - if basis and basis.ui: - # Action aus Menü und Toolbar entfernen + if basis and getattr(basis, "ui", None): basis.ui.remove_action(self.action) self.action = None def run(self): - self.dockwidget = DockWidget(self.iface.mainWindow(), subtitle=self.plugin_name) + self.dockwidget = DockWidget( + self.iface.mainWindow(), + subtitle=self.plugin_name, + ) self.dockwidget.setObjectName(self.dock_name) # Action-Referenz im Dock speichern @@ -48,5 +55,5 @@ class Plan41: # Toolbar-Button als aktiv markieren basis = plugins.get("sn_basis") - if basis and basis.ui: + if basis and getattr(basis, "ui", None): basis.ui.set_active_plugin(self.action) diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 0000000..17b70df --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,3 @@ +{ + "extraPaths": ["."] +} diff --git a/test/run_tests.py b/test/run_tests.py deleted file mode 100644 index f61f42f..0000000 --- a/test/run_tests.py +++ /dev/null @@ -1,27 +0,0 @@ -import unittest -import os -import sys - -# Plugin-Hauptverzeichnis ermitteln -BASE_DIR = os.path.abspath(os.path.dirname(__file__)) - -# Plugin-Ordner in den Python-Pfad aufnehmen -sys.path.insert(0, BASE_DIR) - -def run(): - # Testverzeichnis - test_dir = os.path.join(BASE_DIR, "tests") - - # Test-Suite automatisch finden - suite = unittest.defaultTestLoader.discover(test_dir) - - # Runner starten - runner = unittest.TextTestRunner(verbosity=2) - result = runner.run(suite) - - # Exit-Code setzen (für CI oder Skripte nützlich) - sys.exit(not result.wasSuccessful()) - - -if __name__ == "__main__": - run() diff --git a/test/test_tab_a.py b/test/test_tab_a.py deleted file mode 100644 index 4524392..0000000 --- a/test/test_tab_a.py +++ /dev/null @@ -1,112 +0,0 @@ -import unittest -import os -import tempfile -import sys - -# Plugin-Ordner in den Python-Pfad aufnehmen -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -from qgis.PyQt.QtWidgets import QApplication -from qgis.core import QgsProject, QgsVectorLayer - -from sn_plan41.ui.tabs import tab_a - - -class TestTabA(unittest.TestCase): - - @classmethod - def setUpClass(cls): - """Qt-Anwendung initialisieren.""" - cls.app = QApplication([]) - - def setUp(self): - """Vor jedem Test: Projektvariablen löschen und TabA neu erzeugen.""" - self.project = QgsProject.instance() - self.project.setCustomVariables({}) - - # TabA erzeugen - self.tab = TabA() - - # Temporäre Testdateien - self.tmp_dir = tempfile.gettempdir() - self.test_db = os.path.join(self.tmp_dir, "test_db.gpkg") - self.test_link = os.path.join(self.tmp_dir, "test_link.xlsx") - - # Dummy-Dateien anlegen - with open(self.test_db, "w") as f: - f.write("") - with open(self.test_link, "w") as f: - f.write("") - - # --------------------------------------------------------- - # Verfahrens-DB speichern & wiederherstellen - # --------------------------------------------------------- - def test_save_and_restore_verfahrens_db(self): - self.tab.on_file_changed(self.test_db) - - vars = self.project.customVariables() - self.assertEqual(vars.get("sn_verfahrens_db"), self.test_db) - - tab2 = TabA() - self.assertEqual(tab2.verfahrens_db, self.test_db) - self.assertEqual(tab2.file_widget.filePath(), self.test_db) - - # --------------------------------------------------------- - # Verfahrens-DB löschen - # --------------------------------------------------------- - def test_delete_verfahrens_db(self): - self.tab.on_file_changed(self.test_db) - self.tab.on_file_changed("") - - vars = self.project.customVariables() - self.assertNotIn("sn_verfahrens_db", vars) - self.assertIsNone(self.tab.verfahrens_db) - - # --------------------------------------------------------- - # Linkliste speichern & löschen - # --------------------------------------------------------- - def test_save_and_delete_linkliste(self): - self.tab.on_linkliste_changed(self.test_link) - - vars = self.project.customVariables() - self.assertEqual(vars.get("sn_linkliste"), self.test_link) - - self.tab.on_linkliste_changed("") - - vars = self.project.customVariables() - self.assertNotIn("sn_linkliste", vars) - - # --------------------------------------------------------- - # Layer-Vorauswahl - # --------------------------------------------------------- - def test_preselect_verfahrensgebiet_layer(self): - vg_layer = QgsVectorLayer("Polygon?crs=EPSG:4326", "Verfahrensgebiet", "memory") - other_layer = QgsVectorLayer("Polygon?crs=EPSG:4326", "AndereDaten", "memory") - - self.project.addMapLayer(other_layer) - self.project.addMapLayer(vg_layer) - - tab2 = TabA() - - selected = tab2.layer_combo.currentLayer() - self.assertIsNotNone(selected) - self.assertEqual(selected.name(), "Verfahrensgebiet") - - # --------------------------------------------------------- - # Gespeicherter Layer wird wiederhergestellt - # --------------------------------------------------------- - def test_restore_saved_layer(self): - vg_layer = QgsVectorLayer("Polygon?crs=EPSG:4326", "Verfahrensgebiet", "memory") - self.project.addMapLayer(vg_layer) - - vars = {"sn_verfahrensgebiet_layer": vg_layer.id()} - self.project.setCustomVariables(vars) - - tab2 = TabA() - - selected = tab2.layer_combo.currentLayer() - self.assertEqual(selected.id(), vg_layer.id()) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/__init__.py b/tests/__init__.py similarity index 100% rename from test/__init__.py rename to tests/__init__.py diff --git a/tests/run_tests.py b/tests/run_tests.py new file mode 100644 index 0000000..42e1cdd --- /dev/null +++ b/tests/run_tests.py @@ -0,0 +1,148 @@ +""" +sn_plan41/test/run_tests.py + +Zentraler Test-Runner für sn_plan41. +Wrapper-konform, QGIS-unabhängig, CI- und IDE-fähig. +""" + +import unittest +import datetime +import inspect +import os +import sys +from pathlib import Path + + +# --------------------------------------------------------- +# Plugin-Roots bestimmen +# --------------------------------------------------------- + +THIS_FILE = Path(__file__).resolve() + +# .../plugins/sn_plan41 +SN_PLAN41_ROOT = THIS_FILE.parents[1] +# .../plugins/sn_basis +SN_BASIS_ROOT = SN_PLAN41_ROOT.parent / "sn_basis" + +# --------------------------------------------------------- +# sys.path Bootstrap +# --------------------------------------------------------- + +for path in (SN_PLAN41_ROOT, SN_BASIS_ROOT): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + + +# --------------------------------------------------------- +# Farben +# --------------------------------------------------------- + +RED = "\033[91m" +YELLOW = "\033[93m" +GREEN = "\033[92m" +CYAN = "\033[96m" +MAGENTA = "\033[95m" +RESET = "\033[0m" + +GLOBAL_TEST_COUNTER = 0 + + +# --------------------------------------------------------- +# Farbige TestResult-Klasse +# --------------------------------------------------------- + +class ColoredTestResult(unittest.TextTestResult): + + _last_test_class = None + + def startTest(self, test): + global GLOBAL_TEST_COUNTER + GLOBAL_TEST_COUNTER += 1 + self.stream.write(f"{CYAN}[Test {GLOBAL_TEST_COUNTER}]{RESET}\n") + super().startTest(test) + + def startTestClass(self, test): + cls = test.__class__ + file = inspect.getfile(cls) + filename = os.path.basename(file) + + self.stream.write( + f"\n{MAGENTA}{'=' * 70}\n" + f"Starte Testklasse: {filename} → {cls.__name__}\n" + f"{'=' * 70}{RESET}\n" + ) + + def addError(self, test, err): + super().addError(test, err) + self.stream.write(f"{RED}ERROR{RESET}\n") + + def addFailure(self, test, err): + super().addFailure(test, err) + self.stream.write(f"{RED}FAILURE{RESET}\n") + + def addSkip(self, test, reason): + super().addSkip(test, reason) + self.stream.write(f"{YELLOW}SKIPPED{RESET}: {reason}\n") + + def addSuccess(self, test): + super().addSuccess(test) + self.stream.write(f"{GREEN}OK{RESET}\n") + + +# --------------------------------------------------------- +# Farbiger TestRunner +# --------------------------------------------------------- + +class ColoredTestRunner(unittest.TextTestRunner): + + def _makeResult(self): + result = ColoredTestResult( + self.stream, + self.descriptions, + self.verbosity, + ) + + original_start_test = result.startTest + + def patched_start_test(test): + if result._last_test_class != test.__class__: + result.startTestClass(test) + result._last_test_class = test.__class__ + original_start_test(test) + + result.startTest = patched_start_test + return result + + +# --------------------------------------------------------- +# Testlauf starten +# --------------------------------------------------------- + +def main(): + print("\n" + "=" * 70) + print( + f"{CYAN}Testlauf gestartet am: " + f"{datetime.datetime.now():%Y-%m-%d %H:%M:%S}{RESET}" + ) + print("=" * 70 + "\n") + + loader = unittest.TestLoader() + + TEST_ROOT = SN_PLAN41_ROOT / "tests" + + suite = loader.discover( + start_dir=str(TEST_ROOT), + pattern="test_*.py", + top_level_dir=str(SN_PLAN41_ROOT.parent), +) + + + runner = ColoredTestRunner(verbosity=2) + result = runner.run(suite) + + return 0 if result.wasSuccessful() else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test/start_osgeo4w_qgis.bat b/tests/start_osgeo4w_qgis.bat similarity index 100% rename from test/start_osgeo4w_qgis.bat rename to tests/start_osgeo4w_qgis.bat diff --git a/tests/test_tab_a_logic.py b/tests/test_tab_a_logic.py new file mode 100644 index 0000000..eed5bd4 --- /dev/null +++ b/tests/test_tab_a_logic.py @@ -0,0 +1,153 @@ +import unittest +from pathlib import Path +from tempfile import TemporaryDirectory + +from sn_plan41.ui.tab_a_logic import TabALogic # type: ignore +from sn_basis.functions.variable_wrapper import get_variable # type: ignore +from sn_basis.functions.sys_wrapper import file_exists # type: ignore + + +class TestTabALogic(unittest.TestCase): + + # ----------------------------------------------------- + # 1. Verfahrens-DB setzen und laden + # ----------------------------------------------------- + def test_verfahrens_db_set_and_load(self): + logic = TabALogic() + + with TemporaryDirectory() as tmp: + db_path = Path(tmp) / "test.gpkg" + db_path.write_text("") + + logic.set_verfahrens_db(str(db_path)) + + stored = get_variable("verfahrens_db", scope="project") + self.assertEqual(stored, str(db_path)) + + loaded = logic.load_verfahrens_db() + self.assertEqual(loaded, str(db_path)) + + # ----------------------------------------------------- + # 2. Verfahrens-DB löschen + # ----------------------------------------------------- + def test_verfahrens_db_clear(self): + logic = TabALogic() + + logic.set_verfahrens_db(None) + + stored = get_variable("verfahrens_db", scope="project") + self.assertEqual(stored, "") + + # ----------------------------------------------------- + # 3. Neue Verfahrens-DB anlegen + # ----------------------------------------------------- + def test_create_new_verfahrens_db(self): + logic = TabALogic() + + with TemporaryDirectory() as tmp: + db_path = Path(tmp) / "neu.gpkg" + + result = logic.create_new_verfahrens_db(str(db_path)) + + self.assertTrue(result) + self.assertTrue(file_exists(db_path)) + + stored = get_variable("verfahrens_db", scope="project") + self.assertEqual(stored, str(db_path)) + + def test_create_new_verfahrens_db_with_none_path(self): + logic = TabALogic() + result = logic.create_new_verfahrens_db(None) + self.assertFalse(result) + + + + # ----------------------------------------------------- + # 4. Linkliste setzen und laden + # ----------------------------------------------------- + def test_linkliste_set_and_load(self): + logic = TabALogic() + + with TemporaryDirectory() as tmp: + link_path = Path(tmp) / "links.xlsx" + link_path.write_text("dummy") + + logic.set_linkliste(str(link_path)) + + stored = get_variable("linkliste", scope="project") + self.assertEqual(stored, str(link_path)) + + loaded = logic.load_linkliste() + self.assertEqual(loaded, str(link_path)) + + # ----------------------------------------------------- + # 5. Linkliste löschen + # ----------------------------------------------------- + def test_linkliste_clear(self): + logic = TabALogic() + + logic.set_linkliste(None) + + stored = get_variable("linkliste", scope="project") + self.assertEqual(stored, "") + + # ----------------------------------------------------- + # 6. Layer-ID speichern + # ----------------------------------------------------- + def test_verfahrensgebiet_layer_id_storage(self): + logic = TabALogic() + + class MockLayer: + def id(self): + return "layer-123" + + logic.save_verfahrensgebiet_layer(MockLayer()) + + stored = get_variable("verfahrensgebiet_layer", scope="project") + self.assertEqual(stored, "layer-123") + + # ----------------------------------------------------- + # 7. Ungültiger Layer wird ignoriert + # ----------------------------------------------------- + def test_invalid_layer_is_rejected(self): + logic = TabALogic() + + class InvalidLayer: + pass + + logic.save_verfahrensgebiet_layer(InvalidLayer()) + + stored = get_variable("verfahrensgebiet_layer", scope="project") + self.assertEqual(stored, "") + #----------------------------------------------------- + # 8. Layer-ID wirft Exception + #---------------------------------------------------- + def test_layer_id_raises_exception(self): + logic = TabALogic() + + class BadLayer: + def id(self): + raise RuntimeError("boom") + + logic.save_verfahrensgebiet_layer(BadLayer()) + + stored = get_variable("verfahrensgebiet_layer", scope="project") + self.assertEqual(stored, "") + # ----------------------------------------------------- + # 11. Layer ID wird leer zurückgegeben + # ----------------------------------------------------- + def test_layer_id_returns_empty(self): + logic = TabALogic() + + class EmptyLayer: + def id(self): + return "" + + logic.save_verfahrensgebiet_layer(EmptyLayer()) + + stored = get_variable("verfahrensgebiet_layer", scope="project") + self.assertEqual(stored, "") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_tab_a_ui.py b/tests/test_tab_a_ui.py new file mode 100644 index 0000000..4911949 --- /dev/null +++ b/tests/test_tab_a_ui.py @@ -0,0 +1,57 @@ +""" +Smoke-Tests für TabA UI (sn_plan41/ui/tab_a_ui.py) + +Ziel: +- UI kann erstellt werden +- Callbacks crashen nicht +- Keine Qt-Verhaltensprüfung +""" + +import unittest + +from sn_plan41.ui.tab_a_ui import TabA #type:ignore + + +class TestTabAUI(unittest.TestCase): + + # ----------------------------------------------------- + # 1. UI kann erstellt werden + # ----------------------------------------------------- + def test_tab_a_ui_can_be_created(self): + tab = TabA(parent=None,build_ui=False) + + self.assertIsNotNone(tab) + self.assertEqual(tab.tab_title, "Daten") + + # ----------------------------------------------------- + # 2. Toggle-Callbacks crashen nicht + # ----------------------------------------------------- + def test_tab_a_toggle_callbacks_do_not_crash(self): + tab = TabA(parent=None,build_ui=False) + + tab._toggle_group(True) + tab._toggle_group(False) + + tab._toggle_optional(True) + tab._toggle_optional(False) + + # ----------------------------------------------------- + # 3. Datei-Callbacks akzeptieren leere Eingaben + # ----------------------------------------------------- + def test_tab_a_file_callbacks_accept_empty_input(self): + tab = TabA(parent=None,build_ui=False) + + tab._on_verfahrens_db_changed("") + tab._on_linkliste_changed("") + + # ----------------------------------------------------- + # 4. Layer-Callback akzeptiert None + # ----------------------------------------------------- + def test_tab_a_layer_callback_accepts_none(self): + tab = TabA(parent=None,build_ui=False) + + tab._on_layer_changed(None) + + +if __name__ == "__main__": + unittest.main() diff --git a/ui/dockwidget.py b/ui/dockwidget.py index 5ec1750..9f74104 100644 --- a/ui/dockwidget.py +++ b/ui/dockwidget.py @@ -1,7 +1,7 @@ -from sn_basis.ui.tabs.settings_tab import SettingsTab -from sn_plan41.ui.tabs.tab_a import TabA -from sn_plan41.ui.tabs.tab_b import TabB -from sn_basis.ui.base_dockwidget import BaseDockWidget +from sn_basis.ui.tabs.settings_tab import SettingsTab +from sn_plan41.ui.tab_a_ui import TabA +#from sn_plan41.ui.tabs.tab_b import TabB +from sn_basis.ui.base_dockwidget import BaseDockWidget class DockWidget(BaseDockWidget): - tabs = [TabA, TabB, SettingsTab] + tabs = [TabA, SettingsTab] diff --git a/ui/tab_a_logic.py b/ui/tab_a_logic.py new file mode 100644 index 0000000..e930cb5 --- /dev/null +++ b/ui/tab_a_logic.py @@ -0,0 +1,132 @@ +""" +sn_plan41/ui/tab_a_logic.py – Fachlogik für Tab A (Daten) +""" + +from typing import Optional + +from sn_basis.functions.variable_wrapper import (#type: ignore + get_variable, + set_variable, +) +from sn_basis.functions.sys_wrapper import (#type:ignore + file_exists, + write_text, +) +from sn_basis.functions.ly_existence_wrapper import layer_exists#type:ignore +from sn_basis.functions.ly_metadata_wrapper import get_layer_type#type:ignore + + +class TabALogic: + """ + Kapselt die komplette Logik von Tab A: + - Verfahrens-Datenbank + - optionale Linkliste + - Verfahrensgebiet-Layer + """ + + # ------------------------------- + # Verfahrens-Datenbank + # ------------------------------- + + def load_verfahrens_db(self) -> Optional[str]: + """ + Lädt die gespeicherte Verfahrens-Datenbank. + """ + path = get_variable("verfahrens_db", scope="project") + if path and file_exists(path): + return path + return None + + def set_verfahrens_db(self, path: Optional[str]) -> None: + """ + Speichert oder löscht die Verfahrens-Datenbank. + """ + if path: + set_variable("verfahrens_db", path, scope="project") + else: + set_variable("verfahrens_db", "", scope="project") + + def create_new_verfahrens_db(self, path: str) -> bool: + """ + Legt eine neue leere GPKG-Datei an. + """ + if not path: + return False + + try: + write_text(path, "") + except Exception: + return False + + self.set_verfahrens_db(path) + return True + + + # ------------------------------- + # Lokale Linkliste + # ------------------------------- + + def load_linkliste(self) -> Optional[str]: + """ + Lädt die gespeicherte lokale Linkliste. + """ + path = get_variable("linkliste", scope="project") + if path and file_exists(path): + return path + return None + + def set_linkliste(self, path: Optional[str]) -> None: + """ + Speichert oder löscht die lokale Linkliste. + """ + if path: + set_variable("linkliste", path, scope="project") + else: + set_variable("linkliste", "", scope="project") + + # ------------------------------- + # Verfahrensgebiet-Layer + # ------------------------------- + + def save_verfahrensgebiet_layer(self, layer) -> None: + """ + Speichert die ID des Verfahrensgebiet-Layers. + Ungültige Layer werden ignoriert. + """ + if layer is None: + set_variable("verfahrensgebiet_layer", "", scope="project") + return + + if not hasattr(layer, "id") or not callable(layer.id): + set_variable("verfahrensgebiet_layer", "", scope="project") + return + + try: + layer_id = layer.id() + except Exception: + set_variable("verfahrensgebiet_layer", "", scope="project") + return + + if not layer_id: + set_variable("verfahrensgebiet_layer", "", scope="project") + return + + set_variable("verfahrensgebiet_layer", layer_id, scope="project") + + + def load_verfahrensgebiet_layer_id(self) -> Optional[str]: + """ + Lädt die gespeicherte Layer-ID. + """ + value = get_variable("verfahrensgebiet_layer", scope="project") + return value or None + + def is_valid_verfahrensgebiet_layer(self, layer) -> bool: + """ + Prüft, ob ein Layer als Verfahrensgebiet geeignet ist. + """ + if not layer_exists(layer): + return False + + layer_type = get_layer_type(layer) + return layer_type == "vector" diff --git a/ui/tab_a_ui.py b/ui/tab_a_ui.py new file mode 100644 index 0000000..a692f25 --- /dev/null +++ b/ui/tab_a_ui.py @@ -0,0 +1,308 @@ +""" +sn_plan41/ui/tab_a_ui.py – UI für Tab A (Daten) +""" + +from typing import Optional + +from sn_basis.functions.qt_wrapper import ( # type: ignore + QWidget, + QVBoxLayout, + QLabel, + QPushButton, + QToolButton, + QFileDialog, + QMessageBox, + QTabWidget, + ToolButtonTextBesideIcon, + ArrowDown, + ArrowRight, + SizePolicyPreferred, + SizePolicyMaximum, + +) +from sn_basis.functions.qgisui_wrapper import ( # type: ignore + QgsFileWidget, + QgsMapLayerComboBox, + add_dock_widget, +) +from sn_basis.functions.qgiscore_wrapper import ( # type: ignore + QgsProject, + QgsMapLayerProxyModel, +) +from sn_basis.functions.message_wrapper import ( # type: ignore + info, + warning, + error, +) +from sn_basis.functions.dialog_wrapper import ask_yes_no # type: ignore +from sn_basis.functions.sys_wrapper import file_exists # type: ignore + +from sn_plan41.ui.tab_a_logic import TabALogic # type: ignore + + +class TabA(QWidget): + """ + UI-Klasse für Tab A (Daten). + Enthält ausschließlich UI-Code und delegiert Logik an TabALogic. + """ + + + + def __init__(self, parent=None, build_ui: bool=True): + super().__init__(parent) + self.parent=parent + self.tab_title="Daten" + + self.logic = TabALogic() + + self.verfahrens_db: Optional[str] = None + self.lokale_linkliste: Optional[str] = None + + if build_ui: + self._build_ui() + self._restore_state() + + # --------------------------------------------------------- + # UI-Aufbau + # --------------------------------------------------------- + + def _build_ui(self) -> None: + main_layout = QVBoxLayout() + main_layout.setSpacing(4) + main_layout.setContentsMargins(4, 4, 4, 4) + + # ------------------------------- + # Verfahrens-Datenbank + # ------------------------------- + + self.group_button = QToolButton() + self.group_button.setText("Verfahrens-Datenbank") + self.group_button.setCheckable(True) + self.group_button.setChecked(True) + self.group_button.setToolButtonStyle(ToolButtonTextBesideIcon) + self.group_button.setArrowType(ArrowDown) + + self.group_button.setStyleSheet("font-weight: bold;") + self.group_button.toggled.connect(self._toggle_group) + main_layout.addWidget(self.group_button) + + self.group_content = QWidget() + self.group_content.setSizePolicy(SizePolicyPreferred, SizePolicyMaximum) + + + group_layout = QVBoxLayout() + group_layout.setSpacing(2) + group_layout.setContentsMargins(10, 4, 4, 4) + + group_layout.addWidget(QLabel("bestehende Datei auswählen")) + + self.file_widget = QgsFileWidget() + self.file_widget.setStorageMode(QgsFileWidget.GetFile) + self.file_widget.setFilter("Geopackage (*.gpkg)") + self.file_widget.fileChanged.connect(self._on_verfahrens_db_changed) + group_layout.addWidget(self.file_widget) + + group_layout.addWidget(QLabel("-oder-")) + + self.btn_new = QPushButton("Neue Verfahrens-DB anlegen") + self.btn_new.clicked.connect(self._create_new_gpkg) + group_layout.addWidget(self.btn_new) + + self.group_content.setLayout(group_layout) + main_layout.addWidget(self.group_content) + + # ------------------------------- + # Optionale Linkliste + # ------------------------------- + + self.optional_button = QToolButton() + self.optional_button.setText("Optional: Lokale Linkliste") + self.optional_button.setCheckable(True) + self.optional_button.setChecked(False) + self.optional_button.setToolButtonStyle(ToolButtonTextBesideIcon) + self.optional_button.setArrowType(ArrowRight) + + self.optional_button.setStyleSheet("font-weight: bold; margin-top: 6px;") + self.optional_button.toggled.connect(self._toggle_optional) + main_layout.addWidget(self.optional_button) + + self.optional_content = QWidget() + self.optional_content.setSizePolicy(SizePolicyPreferred, SizePolicyMaximum) + + optional_layout = QVBoxLayout() + optional_layout.setSpacing(2) + optional_layout.setContentsMargins(10, 4, 4, 20) + + optional_layout.addWidget(QLabel("(frei lassen für globale Linkliste)")) + + self.linkliste_widget = QgsFileWidget() + self.linkliste_widget.setStorageMode(QgsFileWidget.GetFile) + self.linkliste_widget.setFilter("Excelliste (*.xlsx)") + self.linkliste_widget.fileChanged.connect(self._on_linkliste_changed) + optional_layout.addWidget(self.linkliste_widget) + + self.optional_content.setLayout(optional_layout) + self.optional_content.setVisible(False) + main_layout.addWidget(self.optional_content) + + # ------------------------------- + # Layer-Auswahl + # ------------------------------- + + layer_label = QLabel("Verfahrensgebiet-Layer auswählen") + layer_label.setStyleSheet("font-weight: bold; margin-top: 6px;") + main_layout.addWidget(layer_label) + + self.layer_combo = QgsMapLayerComboBox() + self.layer_combo.setSizePolicy(SizePolicyPreferred, SizePolicyMaximum) + self.layer_combo.setFilters(QgsMapLayerProxyModel.VectorLayer) + self.layer_combo.layerChanged.connect(self._on_layer_changed) + main_layout.addWidget(self.layer_combo) + + main_layout.addStretch(1) + self.setLayout(main_layout) + + # --------------------------------------------------------- + # State Restore + # --------------------------------------------------------- + + def _restore_state(self) -> None: + db = self.logic.load_verfahrens_db() + if db: + self.verfahrens_db = db + self.file_widget.setFilePath(db) + self._update_group_color() + + link = self.logic.load_linkliste() + if link: + self.lokale_linkliste = link + self.linkliste_widget.setFilePath(link) + + layer_id = self.logic.load_verfahrensgebiet_layer_id() + if layer_id: + layer = QgsProject.instance().mapLayer(layer_id) + if layer: + self.layer_combo.setLayer(layer) + + # --------------------------------------------------------- + # UI-Callbacks + # --------------------------------------------------------- + + def _toggle_group(self, checked: bool): + """ + Klappt den Gruppenbereich ein oder aus. + """ + if not hasattr(self, "group_button"): + return + + self.group_button.setArrowType( + ArrowDown if checked else ArrowRight +) + + self.group_content.setVisible(checked) + + + def _toggle_optional(self, checked: bool): + """ + Klappt den optionalen Bereich ein oder aus. + """ + if not hasattr(self, "optional_button"): + return + + self.group_button.setArrowType( + ArrowDown if checked else ArrowRight +) + + self.optional_content.setVisible(checked) + + + def _on_verfahrens_db_changed(self, path: str) -> None: + if not path: + self.verfahrens_db = None + self.logic.set_verfahrens_db(None) + self._update_group_color() + return + + if not path.lower().endswith(".gpkg"): + path += ".gpkg" + self.file_widget.setFilePath(path) + + if not file_exists(path): + warning("Datei nicht gefunden", f"Die Datei existiert nicht:\n{path}") + self.file_widget.setFilePath("") + return + + self.verfahrens_db = path + self.logic.set_verfahrens_db(path) + self._update_group_color() + + def _on_linkliste_changed(self, path: str) -> None: + if not path: + self.lokale_linkliste = None + self.logic.set_linkliste(None) + return + + if not path.lower().endswith(".xlsx"): + path += ".xlsx" + self.linkliste_widget.setFilePath(path) + + if not file_exists(path): + warning("Datei nicht gefunden", f"Die Datei existiert nicht:\n{path}") + self.linkliste_widget.setFilePath("") + return + + self.lokale_linkliste = path + self.logic.set_linkliste(path) + + def _on_layer_changed(self, layer) -> None: + self.logic.save_verfahrensgebiet_layer(layer) + + def _create_new_gpkg(self) -> None: + file_path, _ = QFileDialog.getSaveFileName( + self, + "Neue Verfahrens-Datenbank anlegen", + "", + "Geopackage (*.gpkg)", + ) + + if not file_path: + return + + if not file_path.lower().endswith(".gpkg"): + file_path += ".gpkg" + + if file_exists(file_path): + overwrite = ask_yes_no( + "Datei existiert bereits", + f"Die Datei existiert bereits:\n\n{file_path}\n\nSoll sie überschrieben werden?", + default=False, + parent=self, + ) + if not overwrite: + return + + if not self.logic.create_new_verfahrens_db(file_path): + error("Fehler", "Die Datei konnte nicht angelegt werden.") + return + + self.verfahrens_db = file_path + self.file_widget.setFilePath(file_path) + self._update_group_color() + info("Projekt-DB angelegt", f"Neue Projekt-Datenbank wurde angelegt:\n{file_path}") + + # --------------------------------------------------------- + # UI-Helfer + # --------------------------------------------------------- + + def _update_group_color(self): + """ + Aktualisiert die Darstellung der Gruppenüberschrift. + """ + if not hasattr(self, "group_button"): + return + + if self.verfahrens_db: + self.group_button.setStyleSheet("font-weight: bold;") + else: + self.group_button.setStyleSheet("") + diff --git a/ui/tabs/tab_a.py b/ui/tabs/tab_a.py deleted file mode 100644 index 7480955..0000000 --- a/ui/tabs/tab_a.py +++ /dev/null @@ -1,316 +0,0 @@ -import os - -from qgis.PyQt.QtWidgets import ( - QWidget, QVBoxLayout, QLabel, QMessageBox, QPushButton, - QFileDialog, QToolButton, QSizePolicy -) -from qgis.PyQt.QtCore import Qt -from qgis.gui import QgsFileWidget, QgsMapLayerComboBox -from qgis.core import QgsProject, QgsMapLayerProxyModel - - -class TabA(QWidget): - tab_title = "Daten" - - def __init__(self, parent=None): - super().__init__(parent) - - # Variablen initialisieren - self.verfahrens_db = None - self.lokale_linkliste = None - - # --------------------------------------------------------- - # Hauptlayout - # --------------------------------------------------------- - main_layout = QVBoxLayout() - main_layout.setSpacing(4) - main_layout.setContentsMargins(4, 4, 4, 4) - - # --------------------------------------------------------- - # COLLAPSIBLE GRUPPE: Verfahrens-Datenbank - # --------------------------------------------------------- - self.group_button = QToolButton() - self.group_button.setText("Verfahrens-Datenbank") - self.group_button.setCheckable(True) - self.group_button.setChecked(True) - self.group_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) - self.group_button.setArrowType(Qt.DownArrow) - self.group_button.setStyleSheet("font-weight: bold;") - self.group_button.toggled.connect(self.toggle_group) - main_layout.addWidget(self.group_button, 0) - - # Inhalt der Gruppe - self.group_content = QWidget() - self.group_content.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) - - group_layout = QVBoxLayout() - group_layout.setSpacing(2) - group_layout.setContentsMargins(10, 4, 4, 4) - - # Hinweis - hinweis1 = QLabel("bestehende Datei auswählen") - group_layout.addWidget(hinweis1) - - # Datei-Auswahl - self.file_widget = QgsFileWidget() - self.file_widget.setStorageMode(QgsFileWidget.GetFile) - self.file_widget.setFilter("Geopackage (*.gpkg)") - self.file_widget.fileChanged.connect(self.on_file_changed) - group_layout.addWidget(self.file_widget) - - # Hinweis "-oder-" - hinweis2 = QLabel("-oder-") - group_layout.addWidget(hinweis2) - - # Button: Neue Datei - self.btn_new = QPushButton("Neue Verfahrens-DB anlegen") - self.btn_new.clicked.connect(self.create_new_gpkg) - group_layout.addWidget(self.btn_new) - - self.group_content.setLayout(group_layout) - main_layout.addWidget(self.group_content, 0) - - # --------------------------------------------------------- - # COLLAPSIBLE Optional-Bereich - # --------------------------------------------------------- - self.optional_button = QToolButton() - self.optional_button.setText("Optional: Lokale Linkliste") - self.optional_button.setCheckable(True) - self.optional_button.setChecked(False) - self.optional_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) - self.optional_button.setArrowType(Qt.RightArrow) - self.optional_button.setStyleSheet("font-weight: bold; margin-top: 6px;") - self.optional_button.toggled.connect(self.toggle_optional) - main_layout.addWidget(self.optional_button, 0) - - # Inhalt optional - self.optional_content = QWidget() - self.optional_content.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) - - optional_layout = QVBoxLayout() - optional_layout.setSpacing(2) - optional_layout.setContentsMargins(10, 4, 4, 20) - - # Hinweistext - optional_hint = QLabel("(frei lassen für globale Linkliste)") - optional_layout.addWidget(optional_hint) - - # Datei-Auswahl für Linkliste - self.linkliste_widget = QgsFileWidget() - self.linkliste_widget.setStorageMode(QgsFileWidget.GetFile) - self.linkliste_widget.setFilter("Excelliste (*.xlsx)") - self.linkliste_widget.fileChanged.connect(self.on_linkliste_changed) - optional_layout.addWidget(self.linkliste_widget) - main_layout.addWidget(self.optional_content, 0) - - # --------------------------------------------------------- - # Layer-Auswahlfeld - # --------------------------------------------------------- - layer_label = QLabel("Verfahrensgebiet-Layer auswählen") - layer_label.setStyleSheet("font-weight: bold; margin-top: 6px;") - main_layout.addWidget(layer_label) - - self.layer_combo = QgsMapLayerComboBox() - self.layer_combo.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) - - # ✅ QGIS 3.22–3.46 kompatibel - self.layer_combo.setFilters(QgsMapLayerProxyModel.VectorLayer) - - # Layerwechsel speichern - self.layer_combo.layerChanged.connect(self.on_layer_changed) - - main_layout.addWidget(self.layer_combo) - - self.optional_content.setLayout(optional_layout) - self.optional_content.setVisible(False) - - - # Spacer - main_layout.addStretch(1) - - self.setLayout(main_layout) - - # ✅ gespeicherte Werte wiederherstellen (jetzt existieren die Widgets!) - self.restore_saved_values() - - # ✅ Layer-Vorauswahl durchführen - self.preselect_verfahrensgebiet_layer() - - # --------------------------------------------------------- - # Collapsible Gruppe ein-/ausblenden - # --------------------------------------------------------- - def toggle_group(self, checked): - self.group_button.setArrowType(Qt.DownArrow if checked else Qt.RightArrow) - self.group_content.setVisible(checked) - - def toggle_optional(self, checked): - self.optional_button.setArrowType(Qt.DownArrow if checked else Qt.RightArrow) - self.optional_content.setVisible(checked) - - # --------------------------------------------------------- - # Datei-Auswahl: Verfahrens-DB - # --------------------------------------------------------- - def on_file_changed(self, path: str): - if not path: - self.verfahrens_db = None - - # ✅ Projektvariable löschen - vars = QgsProject.instance().customVariables() - if "sn_verfahrens_db" in vars: - del vars["sn_verfahrens_db"] - QgsProject.instance().setCustomVariables(vars) - - self.update_group_button_color() - return - - if not path.lower().endswith(".gpkg"): - path += ".gpkg" - self.file_widget.setFilePath(path) - - if os.path.exists(path): - self.verfahrens_db = path - else: - self.verfahrens_db = None - QMessageBox.warning(self, "Datei nicht gefunden", f"Die Datei existiert nicht:\n{path}") - self.file_widget.setFilePath("") - - # ✅ speichern - vars = QgsProject.instance().customVariables() - vars["sn_verfahrens_db"] = self.verfahrens_db - QgsProject.instance().setCustomVariables(vars) - - self.update_group_button_color() - - # --------------------------------------------------------- - # Datei-Auswahl: Lokale Linkliste - # --------------------------------------------------------- - def on_linkliste_changed(self, path: str): - if not path: - self.lokale_linkliste = None - - vars = QgsProject.instance().customVariables() - if "sn_linkliste" in vars: - del vars["sn_linkliste"] - QgsProject.instance().setCustomVariables(vars) - - return - - - if not path.lower().endswith(".xlsx"): - path += ".xlsx" - self.linkliste_widget.setFilePath(path) - - if os.path.exists(path): - self.lokale_linkliste = path - else: - self.lokale_linkliste = None - QMessageBox.warning(self, "Datei nicht gefunden", f"Die Datei existiert nicht:\n{path}") - self.linkliste_widget.setFilePath("") - - # ✅ speichern - vars = QgsProject.instance().customVariables() - vars["sn_linkliste"] = self.lokale_linkliste - QgsProject.instance().setCustomVariables(vars) - - # --------------------------------------------------------- - # Layer-Auswahl speichern - # --------------------------------------------------------- - def on_layer_changed(self, layer): - if layer: - vars = QgsProject.instance().customVariables() - vars["sn_verfahrensgebiet_layer"] = layer.id() - QgsProject.instance().setCustomVariables(vars) - - # --------------------------------------------------------- - # Button-Farbe aktualisieren - # --------------------------------------------------------- - def update_group_button_color(self): - if self.verfahrens_db: - self.group_button.setStyleSheet("font-weight: bold; background-color: #c4f7c4;") - else: - self.group_button.setStyleSheet("font-weight: bold;") - - # --------------------------------------------------------- - # Vorauswahl des Layers "Verfahrensgebiet" - # --------------------------------------------------------- - def preselect_verfahrensgebiet_layer(self): - project = QgsProject.instance() - - # ✅ zuerst gespeicherten Layer wiederherstellen - saved_layer_id = project.customVariables().get("sn_verfahrensgebiet_layer", None) - if saved_layer_id: - layer = project.mapLayer(saved_layer_id) - if layer: - self.layer_combo.setLayer(layer) - return - - # ✅ sonst nach Namen suchen - for layer in project.mapLayers().values(): - if "verfahrensgebiet" in layer.name().lower(): - self.layer_combo.setLayer(layer) - return - - # ✅ Fallback: erster Layer - if self.layer_combo.count() > 0: - self.layer_combo.setCurrentIndex(0) - - # --------------------------------------------------------- - # Werte wiederherstellen - # --------------------------------------------------------- - def restore_saved_values(self): - project = QgsProject.instance() - vars = project.customVariables() - - # ✅ Verfahrens-DB - saved_db = vars.get("sn_verfahrens_db", None) - if saved_db and os.path.exists(saved_db): - self.verfahrens_db = saved_db - self.file_widget.setFilePath(saved_db) - self.update_group_button_color() - - # ✅ Linkliste - saved_link = vars.get("sn_linkliste", None) - if saved_link and os.path.exists(saved_link): - self.lokale_linkliste = saved_link - self.linkliste_widget.setFilePath(saved_link) - def create_new_gpkg(self): - """Öffnet einen Save-Dialog und legt eine neue GPKG-Datei an.""" - file_path, _ = QFileDialog.getSaveFileName( - self, - "Neue Verfahrens-Datenbank anlegen", - "", - "Geopackage (*.gpkg);;Alle Dateien (*)" - ) - - if not file_path: - return # Abbruch - - # Automatisch .gpkg anhängen - if not file_path.lower().endswith(".gpkg"): - file_path += ".gpkg" - - # Existiert Datei bereits? - if os.path.exists(file_path): - overwrite = QMessageBox.question( - self, - "Datei existiert bereits", - f"Die Datei existiert bereits:\n\n{file_path}\n\nSoll sie überschrieben werden?", - QMessageBox.Yes | QMessageBox.No, - QMessageBox.No - ) - if overwrite != QMessageBox.Yes: - return - - # Datei anlegen - try: - open(file_path, "w").close() - except Exception as e: - QMessageBox.critical(self, "Fehler", f"Die Datei konnte nicht angelegt werden:\n{e}") - return - - # Datei übernehmen - self.verfahrens_db = file_path - self.file_widget.setFilePath(file_path) - self.update_group_button_color() - - QMessageBox.information(self, "Projekt-DB angelegt", f"Neue Projekt-Datenbank wurde angelegt:\n{file_path}") -- 2.49.1 From e153a45ffaf25a96c36ace91e14291893e4eb3ad Mon Sep 17 00:00:00 2001 From: daniel Date: Fri, 9 Jan 2026 15:19:41 +0100 Subject: [PATCH 08/10] Fix: Bezeichnung des Datentabs (TabA) war TabA anstatt "Daten" --- pyrightconfig.json | 3 --- ui/tab_a_ui.py | 16 ++++++++-------- 2 files changed, 8 insertions(+), 11 deletions(-) delete mode 100644 pyrightconfig.json diff --git a/pyrightconfig.json b/pyrightconfig.json deleted file mode 100644 index 17b70df..0000000 --- a/pyrightconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extraPaths": ["."] -} diff --git a/ui/tab_a_ui.py b/ui/tab_a_ui.py index a692f25..3674db2 100644 --- a/ui/tab_a_ui.py +++ b/ui/tab_a_ui.py @@ -4,7 +4,7 @@ sn_plan41/ui/tab_a_ui.py – UI für Tab A (Daten) from typing import Optional -from sn_basis.functions.qt_wrapper import ( # type: ignore +from sn_basis.functions.qt_wrapper import ( QWidget, QVBoxLayout, QLabel, @@ -20,24 +20,24 @@ from sn_basis.functions.qt_wrapper import ( # type: ignore SizePolicyMaximum, ) -from sn_basis.functions.qgisui_wrapper import ( # type: ignore +from sn_basis.functions.qgisui_wrapper import ( QgsFileWidget, QgsMapLayerComboBox, add_dock_widget, ) -from sn_basis.functions.qgiscore_wrapper import ( # type: ignore +from sn_basis.functions.qgiscore_wrapper import ( QgsProject, QgsMapLayerProxyModel, ) -from sn_basis.functions.message_wrapper import ( # type: ignore +from sn_basis.functions.message_wrapper import ( info, warning, error, ) -from sn_basis.functions.dialog_wrapper import ask_yes_no # type: ignore -from sn_basis.functions.sys_wrapper import file_exists # type: ignore +from sn_basis.functions.dialog_wrapper import ask_yes_no +from sn_basis.functions.sys_wrapper import file_exists -from sn_plan41.ui.tab_a_logic import TabALogic # type: ignore +from sn_plan41.ui.tab_a_logic import TabALogic class TabA(QWidget): @@ -45,7 +45,7 @@ class TabA(QWidget): UI-Klasse für Tab A (Daten). Enthält ausschließlich UI-Code und delegiert Logik an TabALogic. """ - + tab_title = "Daten" def __init__(self, parent=None, build_ui: bool=True): -- 2.49.1 From 93b17e154c1bd9e1aa19e66431fcda5bf68f80ad Mon Sep 17 00:00:00 2001 From: daniel Date: Fri, 13 Feb 2026 21:38:25 +0100 Subject: [PATCH 09/10] =?UTF-8?q?angefangen,=20datagrabber=20anzulegen=20(?= =?UTF-8?q?nicht=20lauff=C3=A4hig)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 8 +- ui/tab_a_logic.py | 135 ++++++++++++++++++- ui/tab_a_ui.py | 321 +++++++++++++++++++++++++++------------------- 3 files changed, 327 insertions(+), 137 deletions(-) diff --git a/main.py b/main.py index 66f2b59..87ab81e 100644 --- a/main.py +++ b/main.py @@ -3,11 +3,15 @@ from qgis.utils import plugins from sn_basis.ui.dockmanager import DockManager from sn_plan41.ui.dockwidget import DockWidget +from sn_basis.modules.DataGrabber import DataGrabber +from sn_basis.modules.Pruefmanager import Pruefmanager class Plan41: def __init__(self, iface): self.iface = iface + self.pruefmanager=Pruefmanager(ui_modus="qgis") + self.data_grabber=DataGrabber(pruefmanager=self.pruefmanager) self.action = None self.dockwidget = None @@ -45,7 +49,9 @@ class Plan41: self.dockwidget = DockWidget( self.iface.mainWindow(), subtitle=self.plugin_name, - ) + pruefmanager=self.pruefmanager, + data_grabber=self.data_grabber) + self.dockwidget.setObjectName(self.dock_name) # Action-Referenz im Dock speichern diff --git a/ui/tab_a_logic.py b/ui/tab_a_logic.py index e930cb5..61d18aa 100644 --- a/ui/tab_a_logic.py +++ b/ui/tab_a_logic.py @@ -2,18 +2,30 @@ sn_plan41/ui/tab_a_logic.py – Fachlogik für Tab A (Daten) """ -from typing import Optional +from __future__ import annotations -from sn_basis.functions.variable_wrapper import (#type: ignore +from typing import Any, Dict, List, Optional, Tuple +from collections.abc import Mapping as _Mapping + +from sn_basis.functions.variable_wrapper import ( # type: ignore get_variable, set_variable, ) -from sn_basis.functions.sys_wrapper import (#type:ignore +from sn_basis.functions.sys_wrapper import ( # type: ignore file_exists, write_text, ) -from sn_basis.functions.ly_existence_wrapper import layer_exists#type:ignore -from sn_basis.functions.ly_metadata_wrapper import get_layer_type#type:ignore +from sn_basis.functions.ly_existence_wrapper import layer_exists # type: ignore +from sn_basis.functions.ly_metadata_wrapper import get_layer_type # type: ignore + +# Prüfer-Typen (werden als Instanzen erwartet) +from sn_basis.modules.Pruefmanager import Pruefmanager # type: ignore +from sn_basis.modules.linkpruefer import Linkpruefer # type: ignore +from sn_basis.modules.stilpruefer import Stilpruefer # type: ignore + +# Typalias für Klarheit +Row = Dict[str, Any] +DataDict = Dict[str, List[Row]] class TabALogic: @@ -22,8 +34,22 @@ class TabALogic: - Verfahrens-Datenbank - optionale Linkliste - Verfahrensgebiet-Layer + + Diese Klasse erwartet beim Erzeugen Instanzen von Pruefmanager, Linkpruefer + und Stilpruefer. Die validate_and_filter_rows-Methode verwendet diese + Instanzen über self. """ + def __init__(self, pruefmanager: Pruefmanager, link_pruefer: Linkpruefer, stil_pruefer: Stilpruefer) -> None: + """ + :param pruefmanager: Instanz des Pruefmanagers (für verarbeite/report) + :param link_pruefer: Instanz des Linkpruefers (mit methode pruefe) + :param stil_pruefer: Instanz des Stilpruefers (mit methode pruefe) + """ + self.pruefmanager = pruefmanager + self.link_pruefer = link_pruefer + self.stil_pruefer = stil_pruefer + # ------------------------------- # Verfahrens-Datenbank # ------------------------------- @@ -61,7 +87,6 @@ class TabALogic: self.set_verfahrens_db(path) return True - # ------------------------------- # Lokale Linkliste # ------------------------------- @@ -113,7 +138,6 @@ class TabALogic: set_variable("verfahrensgebiet_layer", layer_id, scope="project") - def load_verfahrensgebiet_layer_id(self) -> Optional[str]: """ Lädt die gespeicherte Layer-ID. @@ -130,3 +154,100 @@ class TabALogic: layer_type = get_layer_type(layer) return layer_type == "vector" + + # ------------------------------- + # Validierung und Filterung von data_dict + # ------------------------------- + + def validate_and_filter_rows(self, data_dict: DataDict) -> Tuple[DataDict, List[Any]]: + """ + Validiert und filtert die Zeilen aus `data_dict`. + + Erwartete Struktur von `data_dict`: {'rows': [ {attr}, ... ]}. + + Für jede Zeile werden die folgenden Attribute gelesen: + ident = attr['ident'] (Pflicht) + thema = attr['Inhalt'] (optional) + url = attr['Link'] (Pflicht) + stildatei = attr['Stildatei'] (optional) + provider = attr['Provider'] (Pflicht, wird uppercased) + + Verhalten + - Pflichtfelder (ident, Link, Provider) müssen vorhanden und nicht-leer sein, + sonst wird die Zeile verworfen. + - Wenn Link nicht leer ist, wird self.link_pruefer.pruefe(url) aufgerufen. + - Ist das Ergebnis ok: Zeile wird behalten. + - Ist das Ergebnis nicht ok: Zeile wird verworfen; das verarbeitete + pruef_ergebnis wird gesammelt. + - Wenn Stildatei nicht leer ist, wird self.stil_pruefer.pruefe(stildatei) aufgerufen. + - Ist das Ergebnis ok: der Wert bleibt erhalten. + - Ist das Ergebnis nicht ok: das Feld `Stildatei` wird in der zurückgegebenen + Zeile auf None gesetzt; das verarbeitete pruef_ergebnis wird gesammelt. + - Alle pruef_ergebnis-Objekte werden an self.pruefmanager.verarbeite(...) übergeben. + Die verarbeiteten Ergebnisse werden in der Rückgabe-Liste gesammelt. + + Rückgabe + - (valid_data_dict, processed_results) + valid_data_dict: {'rows': [valid_row1, valid_row2, ...]} + processed_results: Liste der vom Pruefmanager verarbeiteten pruef_ergebnis-Objekte + """ + processed_results: List[Any] = [] + valid_rows: List[Row] = [] + + # Grundstruktur prüfen + if not isinstance(data_dict, dict): + return {"rows": []}, processed_results + + rows = data_dict.get("rows", []) + if not isinstance(rows, (list, tuple)): + return {"rows": []}, processed_results + + for raw in rows: + # Sicherstellen, dass raw ein Mapping ist + if not isinstance(raw, _Mapping): + continue + + ident = raw.get("ident") + inhalt = raw.get("Inhalt") + link = raw.get("Link") + stildatei = raw.get("Stildatei") + provider = raw.get("Provider") + + # Pflichtfelder prüfen + if not ident or not link or not provider: + continue + + # Provider normalisieren + provider_norm = str(provider).upper() + + # Link prüfen + pe_link = self.link_pruefer.pruefe(link) + processed_link = self.pruefmanager.verarbeite(pe_link) + if not getattr(processed_link, "ok", False): + processed_results.append(processed_link) + continue # Zeile verwerfen + + # Stil prüfen (falls vorhanden) + if stildatei: + pe_stil = self.stil_pruefer.pruefe(stildatei) + processed_stil = self.pruefmanager.verarbeite(pe_stil) + if not getattr(processed_stil, "ok", False): + processed_results.append(processed_stil) + stildatei_value: Optional[str] = None + else: + stildatei_value = stildatei + else: + stildatei_value = None + + # Validierte Zeile zusammenbauen + validated_row: Row = { + "ident": ident, + "Inhalt": inhalt, + "Link": link, + "Stildatei": stildatei_value, + "Provider": provider_norm, + } + valid_rows.append(validated_row) + + result_dict: DataDict = {"rows": valid_rows} + return result_dict, processed_results diff --git a/ui/tab_a_ui.py b/ui/tab_a_ui.py index 3674db2..bb60bcd 100644 --- a/ui/tab_a_ui.py +++ b/ui/tab_a_ui.py @@ -1,10 +1,9 @@ -""" -sn_plan41/ui/tab_a_ui.py – UI für Tab A (Daten) -""" +# sn_plan41/ui/tab_a_ui.py – UI für Tab A (Daten) +from __future__ import annotations from typing import Optional -from sn_basis.functions.qt_wrapper import ( +from sn_basis.functions.qt_wrapper import ( QWidget, QVBoxLayout, QLabel, @@ -12,52 +11,69 @@ from sn_basis.functions.qt_wrapper import ( QToolButton, QFileDialog, QMessageBox, - QTabWidget, - ToolButtonTextBesideIcon, - ArrowDown, + ToolButtonTextBesideIcon, + ArrowDown, ArrowRight, SizePolicyPreferred, SizePolicyMaximum, + ComboBox, +) +from sn_basis.functions.qgisui_wrapper import QgsFileWidget, QgsMapLayerComboBox +from sn_basis.functions.qgiscore_wrapper import QgsProject, QgsMapLayerProxyModel +from sn_basis.functions.variable_wrapper import get_variable, set_variable -) -from sn_basis.functions.qgisui_wrapper import ( - QgsFileWidget, - QgsMapLayerComboBox, - add_dock_widget, -) -from sn_basis.functions.qgiscore_wrapper import ( - QgsProject, - QgsMapLayerProxyModel, -) -from sn_basis.functions.message_wrapper import ( - info, - warning, - error, -) -from sn_basis.functions.dialog_wrapper import ask_yes_no -from sn_basis.functions.sys_wrapper import file_exists +from sn_plan41.ui.tab_a_logic import TabALogic -from sn_plan41.ui.tab_a_logic import TabALogic +# Prüf‑Workflow / DataGrabber (werden zur Laufzeit vom Pruefmanager/Pruefern verwendet) +from sn_basis.modules.Dateipruefer import Dateipruefer +from sn_basis.modules.Pruefmanager import Pruefmanager +from sn_basis.modules.DataGrabber import DataGrabber +from sn_basis.modules.linkpruefer import Linkpruefer +from sn_basis.modules.stilpruefer import Stilpruefer +# Raumfilter-Optionen +RAUMFILTER_VAR = "Raumfilter" +RAUMFILTER_OPTIONS = ("Verfahrensgebiet", "Pufferlayer", "ohne") +RAUMFILTER_DEFAULT = "Pufferlayer" +pm = Pruefmanager(ui_modus="qgis") +lp = Linkpruefer() +sp = Stilpruefer() class TabA(QWidget): """ UI-Klasse für Tab A (Daten). - Enthält ausschließlich UI-Code und delegiert Logik an TabALogic. + + Diese bereinigte Version enthält ausschließlich UI-Elemente und + einfache, nicht-validierende Callback-Handler. Alle fachlichen Prüfungen + und Fehlerbehandlungen werden zur Laufzeit vom Pruefmanager und den Prüfern + übernommen. """ tab_title = "Daten" - - def __init__(self, parent=None, build_ui: bool=True): + def __init__(self, parent=None, pruefmanager=None, link_pruefer=None, stil_pruefer=None, build_ui=True): super().__init__(parent) - self.parent=parent - self.tab_title="Daten" + self.parent = parent + self.tab_title = "Daten" - self.logic = TabALogic() + # Logik-Adapter (TabALogic verwaltet persistente Projektvariablen) + self.logic = TabALogic(pruefmanager=pruefmanager, link_pruefer=link_pruefer, stil_pruefer=stil_pruefer) + + # Prüfmanager-Instanz (UI-Modus wird zur Laufzeit vom Pruefmanager gehandhabt) + self.pruefmanager = Pruefmanager(ui_modus="qgis") + + # DataGrabber-Instanz (synchroner Aufruf; Prüfungen übernimmt Pruefmanager/Pruefer) + self.data_grabber = DataGrabber(pruefmanager=self.pruefmanager) + + # Platzhalter, die vom Plugin oder Nutzer gesetzt werden können + self._attributes_list = [] # optionale Attributliste (z. B. Excel-Import) + self._pufferlayer = None # optionaler Layer (Verfahrensgebiet) self.verfahrens_db: Optional[str] = None self.lokale_linkliste: Optional[str] = None + # UI-Widget-Referenz für Raumfilter + self._raumfilter_combo: Optional[ComboBox] = None + if build_ui: self._build_ui() self._restore_state() @@ -65,23 +81,18 @@ class TabA(QWidget): # --------------------------------------------------------- # UI-Aufbau # --------------------------------------------------------- - def _build_ui(self) -> None: main_layout = QVBoxLayout() main_layout.setSpacing(4) main_layout.setContentsMargins(4, 4, 4, 4) - # ------------------------------- - # Verfahrens-Datenbank - # ------------------------------- - + # Verfahrens-Datenbank Gruppe self.group_button = QToolButton() self.group_button.setText("Verfahrens-Datenbank") self.group_button.setCheckable(True) self.group_button.setChecked(True) self.group_button.setToolButtonStyle(ToolButtonTextBesideIcon) self.group_button.setArrowType(ArrowDown) - self.group_button.setStyleSheet("font-weight: bold;") self.group_button.toggled.connect(self._toggle_group) main_layout.addWidget(self.group_button) @@ -89,7 +100,6 @@ class TabA(QWidget): self.group_content = QWidget() self.group_content.setSizePolicy(SizePolicyPreferred, SizePolicyMaximum) - group_layout = QVBoxLayout() group_layout.setSpacing(2) group_layout.setContentsMargins(10, 4, 4, 4) @@ -111,17 +121,13 @@ class TabA(QWidget): self.group_content.setLayout(group_layout) main_layout.addWidget(self.group_content) - # ------------------------------- # Optionale Linkliste - # ------------------------------- - self.optional_button = QToolButton() self.optional_button.setText("Optional: Lokale Linkliste") self.optional_button.setCheckable(True) self.optional_button.setChecked(False) self.optional_button.setToolButtonStyle(ToolButtonTextBesideIcon) self.optional_button.setArrowType(ArrowRight) - self.optional_button.setStyleSheet("font-weight: bold; margin-top: 6px;") self.optional_button.toggled.connect(self._toggle_optional) main_layout.addWidget(self.optional_button) @@ -145,10 +151,7 @@ class TabA(QWidget): self.optional_content.setVisible(False) main_layout.addWidget(self.optional_content) - # ------------------------------- # Layer-Auswahl - # ------------------------------- - layer_label = QLabel("Verfahrensgebiet-Layer auswählen") layer_label.setStyleSheet("font-weight: bold; margin-top: 6px;") main_layout.addWidget(layer_label) @@ -159,24 +162,83 @@ class TabA(QWidget): self.layer_combo.layerChanged.connect(self._on_layer_changed) main_layout.addWidget(self.layer_combo) + # Raumfilter-Label + ComboBox (unterhalb der Layer-Auswahl) + main_layout.addWidget(QLabel("Raumfilter")) + self._raumfilter_combo = ComboBox(self) + # Fülle Optionen (Wrapper stellt addItems bereit) + try: + self._raumfilter_combo.addItems(list(RAUMFILTER_OPTIONS)) + except Exception: + # fallback: iterativ hinzufügen, falls Wrapper andere API hat + for opt in RAUMFILTER_OPTIONS: + if hasattr(self._raumfilter_combo, "addItem"): + self._raumfilter_combo.addItem(opt) + + # Initialisiere Auswahl aus Projekt-Variable oder Default + stored = get_variable(RAUMFILTER_VAR, scope="project") + if isinstance(stored, str) and stored in RAUMFILTER_OPTIONS: + try: + self._raumfilter_combo.setCurrentText(stored) + except Exception: + try: + idx = self._raumfilter_combo.findText(stored) + if idx is not None and idx >= 0: + self._raumfilter_combo.setCurrentIndex(idx) + except Exception: + pass + else: + try: + self._raumfilter_combo.setCurrentText(RAUMFILTER_DEFAULT) + except Exception: + try: + idx = self._raumfilter_combo.findText(RAUMFILTER_DEFAULT) + if idx is not None and idx >= 0: + self._raumfilter_combo.setCurrentIndex(idx) + except Exception: + pass + # persistiere Default, falls noch kein Wert gesetzt + if not stored: + set_variable(RAUMFILTER_VAR, RAUMFILTER_DEFAULT, scope="project") + + # Signal: bei Änderung Variable setzen + try: + self._raumfilter_combo.currentTextChanged.connect(self._on_raumfilter_changed) + except Exception: + try: + self._raumfilter_combo.current_text_changed.connect(self._on_raumfilter_changed) + except Exception: + pass + + main_layout.addWidget(self._raumfilter_combo) + + # Aktion: Fachdaten laden + self.btn_load = QPushButton("Fachdaten laden") + self.btn_load.clicked.connect(self._on_load_fachdaten) + main_layout.addWidget(self.btn_load) + main_layout.addStretch(1) self.setLayout(main_layout) # --------------------------------------------------------- - # State Restore + # State Restore (UI-Wiederherstellung ohne Prüfungen) # --------------------------------------------------------- - def _restore_state(self) -> None: db = self.logic.load_verfahrens_db() if db: self.verfahrens_db = db - self.file_widget.setFilePath(db) + try: + self.file_widget.setFilePath(db) + except Exception: + pass self._update_group_color() link = self.logic.load_linkliste() if link: self.lokale_linkliste = link - self.linkliste_widget.setFilePath(link) + try: + self.linkliste_widget.setFilePath(link) + except Exception: + pass layer_id = self.logic.load_verfahrensgebiet_layer_id() if layer_id: @@ -184,78 +246,42 @@ class TabA(QWidget): if layer: self.layer_combo.setLayer(layer) + # Raumfilter aus Variable wiederherstellen (falls Combo existiert) + try: + stored = get_variable(RAUMFILTER_VAR, scope="project") + if stored and self._raumfilter_combo is not None: + try: + self._raumfilter_combo.setCurrentText(stored) + except Exception: + idx = self._raumfilter_combo.findText(stored) + if idx is not None and idx >= 0: + self._raumfilter_combo.setCurrentIndex(idx) + except Exception: + pass + # --------------------------------------------------------- - # UI-Callbacks + # UI-Callbacks (ohne Prüfungen / Exceptions) # --------------------------------------------------------- - - def _toggle_group(self, checked: bool): - """ - Klappt den Gruppenbereich ein oder aus. - """ - if not hasattr(self, "group_button"): - return - - self.group_button.setArrowType( - ArrowDown if checked else ArrowRight -) - + def _toggle_group(self, checked: bool) -> None: + self.group_button.setArrowType(ArrowDown if checked else ArrowRight) self.group_content.setVisible(checked) - - def _toggle_optional(self, checked: bool): - """ - Klappt den optionalen Bereich ein oder aus. - """ - if not hasattr(self, "optional_button"): - return - - self.group_button.setArrowType( - ArrowDown if checked else ArrowRight -) - + def _toggle_optional(self, checked: bool) -> None: + self.optional_button.setArrowType(ArrowDown if checked else ArrowRight) self.optional_content.setVisible(checked) - def _on_verfahrens_db_changed(self, path: str) -> None: - if not path: - self.verfahrens_db = None - self.logic.set_verfahrens_db(None) - self._update_group_color() - return - - if not path.lower().endswith(".gpkg"): - path += ".gpkg" - self.file_widget.setFilePath(path) - - if not file_exists(path): - warning("Datei nicht gefunden", f"Die Datei existiert nicht:\n{path}") - self.file_widget.setFilePath("") - return - self.verfahrens_db = path self.logic.set_verfahrens_db(path) self._update_group_color() def _on_linkliste_changed(self, path: str) -> None: - if not path: - self.lokale_linkliste = None - self.logic.set_linkliste(None) - return - - if not path.lower().endswith(".xlsx"): - path += ".xlsx" - self.linkliste_widget.setFilePath(path) - - if not file_exists(path): - warning("Datei nicht gefunden", f"Die Datei existiert nicht:\n{path}") - self.linkliste_widget.setFilePath("") - return - self.lokale_linkliste = path self.logic.set_linkliste(path) def _on_layer_changed(self, layer) -> None: self.logic.save_verfahrensgebiet_layer(layer) + self._pufferlayer = layer def _create_new_gpkg(self) -> None: file_path, _ = QFileDialog.getSaveFileName( @@ -264,45 +290,82 @@ class TabA(QWidget): "", "Geopackage (*.gpkg)", ) - if not file_path: return if not file_path.lower().endswith(".gpkg"): file_path += ".gpkg" - if file_exists(file_path): - overwrite = ask_yes_no( - "Datei existiert bereits", - f"Die Datei existiert bereits:\n\n{file_path}\n\nSoll sie überschrieben werden?", - default=False, - parent=self, - ) - if not overwrite: - return - - if not self.logic.create_new_verfahrens_db(file_path): - error("Fehler", "Die Datei konnte nicht angelegt werden.") - return - + # Delegation an TabALogic; TabALogic / Pruefmanager übernehmen Prüfungen + self.logic.create_new_verfahrens_db(file_path) self.verfahrens_db = file_path - self.file_widget.setFilePath(file_path) + try: + self.file_widget.setFilePath(file_path) + except Exception: + pass self._update_group_color() - info("Projekt-DB angelegt", f"Neue Projekt-Datenbank wurde angelegt:\n{file_path}") + + def _on_load_fachdaten(self) -> None: + """ + Platzhalter-Handler für 'Fachdaten laden'. + + Keine Prüfungen oder Exception-Handling hier. Die fachliche Prüfung + und Fehlerbehandlung erfolgen zur Laufzeit durch den Pruefmanager und + die Prüfer, die vom DataGrabber verwendet werden. + """ + pfad = self.file_widget.filePath() + + # Dateipruefer wird zur Laufzeit verwendet; hier nur der Aufruf + pruefer = Dateipruefer(pfad=pfad, temporaer_erlaubt=True) + ergebnis = pruefer.pruefe() + ergebnis = self.pruefmanager.verarbeite(ergebnis) + + zielpfad = None + if ergebnis.kontext is not None: + try: + zielpfad = str(ergebnis.kontext) + except Exception: + zielpfad = ergebnis.kontext + + self.data_grabber.run( + attributes_list=self._attributes_list, + pufferlayer=self._pufferlayer, + zielpfad=zielpfad, + temporaer=(ergebnis.aktion == "temporaer_erzeugen"), + temporaer_erlaubt=True, + ) + + # --------------------------------------------------------- + # Raumfilter Callback + # --------------------------------------------------------- + def _on_raumfilter_changed(self, value: str) -> None: + # Persistiere Auswahl in Projekt-Variable; Prüfungen übernimmt die Laufzeitlogik + set_variable(RAUMFILTER_VAR, value, scope="project") # --------------------------------------------------------- # UI-Helfer # --------------------------------------------------------- + def _prompt_user_to_select_file(self) -> None: + fname, _ = QFileDialog.getOpenFileName( + self, + "Verfahrens-DB auswählen", + "", + "Geopackage (*.gpkg)", + ) + if fname: + try: + self.file_widget.setFilePath(fname) + except Exception: + try: + self.file_widget.setFileName(fname) + except Exception: + self.file_widget.setProperty("filePath", fname) + self.verfahrens_db = fname + self.logic.set_verfahrens_db(fname) + self._update_group_color() - def _update_group_color(self): - """ - Aktualisiert die Darstellung der Gruppenüberschrift. - """ - if not hasattr(self, "group_button"): - return - + def _update_group_color(self) -> None: if self.verfahrens_db: self.group_button.setStyleSheet("font-weight: bold;") else: self.group_button.setStyleSheet("") - -- 2.49.1 From b5f663d9de03059232782cfb3acb21aef3e9ed27 Mon Sep 17 00:00:00 2001 From: daniel Date: Sat, 14 Feb 2026 22:15:58 +0100 Subject: [PATCH 10/10] =?UTF-8?q?Button=20Fachdaten=20laden=20hinzugef?= =?UTF-8?q?=C3=BCgt=20und=20angebunden=20(pipeline=20datagrabber-pr=C3=BCf?= =?UTF-8?q?er-datenlader-datenschreiber)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/listenauswerter.py | 137 +++++++++++++++++++++++++++++++ ui/tab_a_logic.py | 97 +--------------------- ui/tab_a_ui.py | 164 ++++++++++++++++++++++++++++++++----- 3 files changed, 282 insertions(+), 116 deletions(-) create mode 100644 modules/listenauswerter.py diff --git a/modules/listenauswerter.py b/modules/listenauswerter.py new file mode 100644 index 0000000..cf9a654 --- /dev/null +++ b/modules/listenauswerter.py @@ -0,0 +1,137 @@ +#sn_plan41/modules/listenauswerter.py +from typing import Any, Dict, List, Mapping, Optional, Tuple +from collections.abc import Mapping as _Mapping +# Prüfer-Typen (werden als Instanzen erwartet) +from sn_basis.modules.Pruefmanager import Pruefmanager # type: ignore +from sn_basis.modules.pruef_ergebnis import pruef_ergebnis +from sn_basis.modules.stilpruefer import Stilpruefer # type: ignore + +class Listenauswerter: + """ + Validiert Zeilen aus einem DataDict, das vom DataGrabber stammt. + Erwartet wird die Struktur:: + + {"rows": [ {attr}, ... ]} + + Die Linkprüfung entfällt vollständig, da der DataGrabber nur gültige + Links liefert. Diese Methode prüft ausschließlich die Konsistenz der + Zeilen mit dem erwarteten Datenschema und führt optional eine + Stilprüfung durch. + """ + def __init__(self, pruefmanager, stil_pruefer): + """ Parameters + ---------- + pruefmanager: Instanz des Pruefmanagers, der pruef_ergebnis verarbeitet. + stil_pruefer: Instanz des Stilpruefers, der Stildateien prüft. + """ + self.pruefmanager = pruefmanager + self.stil_pruefer = stil_pruefer + + def validate_rows( + self, + data_dict: Dict[str, List[Mapping[str, Any]]] + ) -> Tuple[Dict[str, List[Mapping[str, Any]]], List[Any]]: + """ + Validiert die Zeilen aus ``data_dict`` anhand des erwarteten Schemas. + + Erwartete Felder pro Zeile + -------------------------- + Pflichtfelder: + - ``ident``: eindeutige Kennung + - ``Link``: bereits geprüfter Link (vom DataGrabber garantiert gültig) + - ``Provider``: Datenquelle (wird in Großbuchstaben normalisiert) + + Optionale Felder: + - ``Inhalt``: thematische Beschreibung + - ``Stildatei``: Pfad zur Stildatei (falls vorhanden) + + Verhalten + --------- + - Zeilen, denen Pflichtfelder fehlen oder deren Werte leer sind, + werden verworfen. + - ``Provider`` wird in Großbuchstaben normalisiert. + - Wenn ``Stildatei`` vorhanden ist, wird sie durch + ``self.stil_pruefer.pruefe(...)`` geprüft. + - Bei OK bleibt der Wert erhalten. + - Bei nicht OK wird ``Stildatei`` auf ``None`` gesetzt und das + verarbeitete Prüfergebnis gesammelt. + - Alle Prüfergebnisse werden durch ``self.pruefmanager.verarbeite(...)`` + geleitet und in der Rückgabe gesammelt. + + Rückgabe + -------- + Tuple[Dict[str, List[Mapping[str, Any]]]], List[Any]] + - ``valid_data_dict``: enthält nur Zeilen, die dem Schema entsprechen + - ``processed_results``: Liste der verarbeiteten Prüfergebnisse + + Hinweise + -------- + - Diese Methode führt **keine Linkprüfung** durch. + - Die Verantwortung für die Linkvalidität liegt vollständig beim DataGrabber. + - Die Methode verändert die Zeilen nur minimal (Provider‑Normalisierung, + Stildatei ggf. auf ``None``). + """ + + processed_results: List[Any] = [] + valid_rows: List[Mapping[str, Any]] = [] + + # Grundstruktur prüfen + if not isinstance(data_dict, dict): + return {"rows": []}, processed_results + + rows = data_dict.get("rows", []) + if not isinstance(rows, (list, tuple)): + return {"rows": []}, processed_results + + for raw in rows: + # Sicherstellen, dass raw ein Mapping ist + if not isinstance(raw, _Mapping): + continue + + ident = raw.get("ident") + inhalt = raw.get("Inhalt") + link = raw.get("Link") + stildatei = raw.get("Stildatei") + provider = raw.get("Provider") + + # Pflichtfelder prüfen + if not ident or not link or not provider: + # Fehler dokumentieren + pe = pruef_ergebnis( + ok=False, + meldung="Pflichtfelder fehlen oder sind leer", + aktion="pflichtfelder_fehlen", + kontext=raw, + ) + processed_results.append(self.pruefmanager.verarbeite(pe)) + continue + + # Provider normalisieren + provider_norm = str(provider).upper() + + # Stildatei prüfen (falls vorhanden) + if stildatei: + pe_stil = self.stil_pruefer.pruefe(stildatei) + processed_stil = self.pruefmanager.verarbeite(pe_stil) + + if not getattr(processed_stil, "ok", False): + processed_results.append(processed_stil) + stildatei_value: Optional[str] = None + else: + stildatei_value = stildatei + else: + stildatei_value = None + + # Validierte Zeile zusammenbauen + validated_row = { + "ident": ident, + "Inhalt": inhalt, + "Link": link, + "Stildatei": stildatei_value, + "Provider": provider_norm, + } + + valid_rows.append(validated_row) + + result_dict = {"rows": valid_rows} + return result_dict, processed_results diff --git a/ui/tab_a_logic.py b/ui/tab_a_logic.py index 61d18aa..61bdbb7 100644 --- a/ui/tab_a_logic.py +++ b/ui/tab_a_logic.py @@ -155,99 +155,4 @@ class TabALogic: layer_type = get_layer_type(layer) return layer_type == "vector" - # ------------------------------- - # Validierung und Filterung von data_dict - # ------------------------------- - - def validate_and_filter_rows(self, data_dict: DataDict) -> Tuple[DataDict, List[Any]]: - """ - Validiert und filtert die Zeilen aus `data_dict`. - - Erwartete Struktur von `data_dict`: {'rows': [ {attr}, ... ]}. - - Für jede Zeile werden die folgenden Attribute gelesen: - ident = attr['ident'] (Pflicht) - thema = attr['Inhalt'] (optional) - url = attr['Link'] (Pflicht) - stildatei = attr['Stildatei'] (optional) - provider = attr['Provider'] (Pflicht, wird uppercased) - - Verhalten - - Pflichtfelder (ident, Link, Provider) müssen vorhanden und nicht-leer sein, - sonst wird die Zeile verworfen. - - Wenn Link nicht leer ist, wird self.link_pruefer.pruefe(url) aufgerufen. - - Ist das Ergebnis ok: Zeile wird behalten. - - Ist das Ergebnis nicht ok: Zeile wird verworfen; das verarbeitete - pruef_ergebnis wird gesammelt. - - Wenn Stildatei nicht leer ist, wird self.stil_pruefer.pruefe(stildatei) aufgerufen. - - Ist das Ergebnis ok: der Wert bleibt erhalten. - - Ist das Ergebnis nicht ok: das Feld `Stildatei` wird in der zurückgegebenen - Zeile auf None gesetzt; das verarbeitete pruef_ergebnis wird gesammelt. - - Alle pruef_ergebnis-Objekte werden an self.pruefmanager.verarbeite(...) übergeben. - Die verarbeiteten Ergebnisse werden in der Rückgabe-Liste gesammelt. - - Rückgabe - - (valid_data_dict, processed_results) - valid_data_dict: {'rows': [valid_row1, valid_row2, ...]} - processed_results: Liste der vom Pruefmanager verarbeiteten pruef_ergebnis-Objekte - """ - processed_results: List[Any] = [] - valid_rows: List[Row] = [] - - # Grundstruktur prüfen - if not isinstance(data_dict, dict): - return {"rows": []}, processed_results - - rows = data_dict.get("rows", []) - if not isinstance(rows, (list, tuple)): - return {"rows": []}, processed_results - - for raw in rows: - # Sicherstellen, dass raw ein Mapping ist - if not isinstance(raw, _Mapping): - continue - - ident = raw.get("ident") - inhalt = raw.get("Inhalt") - link = raw.get("Link") - stildatei = raw.get("Stildatei") - provider = raw.get("Provider") - - # Pflichtfelder prüfen - if not ident or not link or not provider: - continue - - # Provider normalisieren - provider_norm = str(provider).upper() - - # Link prüfen - pe_link = self.link_pruefer.pruefe(link) - processed_link = self.pruefmanager.verarbeite(pe_link) - if not getattr(processed_link, "ok", False): - processed_results.append(processed_link) - continue # Zeile verwerfen - - # Stil prüfen (falls vorhanden) - if stildatei: - pe_stil = self.stil_pruefer.pruefe(stildatei) - processed_stil = self.pruefmanager.verarbeite(pe_stil) - if not getattr(processed_stil, "ok", False): - processed_results.append(processed_stil) - stildatei_value: Optional[str] = None - else: - stildatei_value = stildatei - else: - stildatei_value = None - - # Validierte Zeile zusammenbauen - validated_row: Row = { - "ident": ident, - "Inhalt": inhalt, - "Link": link, - "Stildatei": stildatei_value, - "Provider": provider_norm, - } - valid_rows.append(validated_row) - - result_dict: DataDict = {"rows": valid_rows} - return result_dict, processed_results + \ No newline at end of file diff --git a/ui/tab_a_ui.py b/ui/tab_a_ui.py index bb60bcd..aaf8716 100644 --- a/ui/tab_a_ui.py +++ b/ui/tab_a_ui.py @@ -16,7 +16,7 @@ from sn_basis.functions.qt_wrapper import ( ArrowRight, SizePolicyPreferred, SizePolicyMaximum, - ComboBox, + QComboBox, ) from sn_basis.functions.qgisui_wrapper import QgsFileWidget, QgsMapLayerComboBox from sn_basis.functions.qgiscore_wrapper import QgsProject, QgsMapLayerProxyModel @@ -30,13 +30,15 @@ from sn_basis.modules.Pruefmanager import Pruefmanager from sn_basis.modules.DataGrabber import DataGrabber from sn_basis.modules.linkpruefer import Linkpruefer from sn_basis.modules.stilpruefer import Stilpruefer +from sn_basis.modules.Datenschreiber import Datenschreiber + # Raumfilter-Optionen RAUMFILTER_VAR = "Raumfilter" RAUMFILTER_OPTIONS = ("Verfahrensgebiet", "Pufferlayer", "ohne") RAUMFILTER_DEFAULT = "Pufferlayer" -pm = Pruefmanager(ui_modus="qgis") -lp = Linkpruefer() -sp = Stilpruefer() +pm = Pruefmanager(ui_modus="qgis") +lp = Linkpruefer() +sp = Stilpruefer() class TabA(QWidget): @@ -62,6 +64,8 @@ class TabA(QWidget): self.pruefmanager = Pruefmanager(ui_modus="qgis") # DataGrabber-Instanz (synchroner Aufruf; Prüfungen übernimmt Pruefmanager/Pruefer) + # Hinweis: DataGrabber erwartet ggf. Prüfer-Objekte; hier werden sie nicht übergeben, + # da TabALogic / Pruefmanager diese zur Laufzeit bereitstellen können. self.data_grabber = DataGrabber(pruefmanager=self.pruefmanager) # Platzhalter, die vom Plugin oder Nutzer gesetzt werden können @@ -72,7 +76,7 @@ class TabA(QWidget): self.lokale_linkliste: Optional[str] = None # UI-Widget-Referenz für Raumfilter - self._raumfilter_combo: Optional[ComboBox] = None + self._raumfilter_combo: Optional[QComboBox] = None if build_ui: self._build_ui() @@ -164,7 +168,7 @@ class TabA(QWidget): # Raumfilter-Label + ComboBox (unterhalb der Layer-Auswahl) main_layout.addWidget(QLabel("Raumfilter")) - self._raumfilter_combo = ComboBox(self) + self._raumfilter_combo = QComboBox(self) # Fülle Optionen (Wrapper stellt addItems bereit) try: self._raumfilter_combo.addItems(list(RAUMFILTER_OPTIONS)) @@ -211,8 +215,14 @@ class TabA(QWidget): main_layout.addWidget(self._raumfilter_combo) - # Aktion: Fachdaten laden - self.btn_load = QPushButton("Fachdaten laden") + # Neuer Button direkt unterhalb der Raumfilter-Combo: "Fachdaten laden" + self.btn_pipeline = QPushButton("Fachdaten laden") + self.btn_pipeline.setToolTip("Starte Pipeline: Linkliste → DataGrabber → Datenschreiber → Log") + self.btn_pipeline.clicked.connect(self._on_run_pipeline) + main_layout.addWidget(self.btn_pipeline) + + # (Optional) bestehender Button weiter unten für alternative Platzierung + self.btn_load = QPushButton("Fachdaten laden (alt)") self.btn_load.clicked.connect(self._on_load_fachdaten) main_layout.addWidget(self.btn_load) @@ -307,11 +317,8 @@ class TabA(QWidget): def _on_load_fachdaten(self) -> None: """ - Platzhalter-Handler für 'Fachdaten laden'. - - Keine Prüfungen oder Exception-Handling hier. Die fachliche Prüfung - und Fehlerbehandlung erfolgen zur Laufzeit durch den Pruefmanager und - die Prüfer, die vom DataGrabber verwendet werden. + Bestehender, kompakter Handler für 'Fachdaten laden'. + Führt Dateiprüfung und DataGrabber.run aus (wie zuvor). """ pfad = self.file_widget.filePath() @@ -327,13 +334,130 @@ class TabA(QWidget): except Exception: zielpfad = ergebnis.kontext - self.data_grabber.run( - attributes_list=self._attributes_list, - pufferlayer=self._pufferlayer, - zielpfad=zielpfad, - temporaer=(ergebnis.aktion == "temporaer_erzeugen"), - temporaer_erlaubt=True, - ) + # DataGrabber.run wird wie bisher aufgerufen; Signatur kann variieren. + # Wir übergeben die bekannten Parameter; DataGrabber ist verantwortlich, + # die Linkliste intern zu verwenden (z. B. aus TabALogic oder über Argumente). + try: + self.data_grabber.run( + attributes_list=self._attributes_list, + pufferlayer=self._pufferlayer, + zielpfad=zielpfad, + temporaer=(ergebnis.aktion == "temporaer_erzeugen"), + temporaer_erlaubt=True, + ) + except Exception: + # Fehler werden vom Pruefmanager / DataGrabber protokolliert + pass + + def _on_run_pipeline(self) -> None: + """ + Neuer, vollständiger Pipeline-Handler, der: + - Dateiprüfung (Verfahrens-DB) + - DataGrabber-Ausführung (mit Linkliste) + - Datenschreiber (schreiben, laden) + - Logschreiber (Log-Datei) + ausführt und Ergebnisse über den Pruefmanager protokolliert. + """ + # 1) Verfahrens-DB prüfen / ermitteln + pfad = self.file_widget.filePath() + pruefer = Dateipruefer(pfad=pfad, temporaer_erlaubt=True) + ergebnis = pruefer.pruefe() + ergebnis = self.pruefmanager.verarbeite(ergebnis) + + zielpfad = None + if ergebnis.kontext is not None: + try: + zielpfad = str(ergebnis.kontext) + except Exception: + zielpfad = ergebnis.kontext + + if not zielpfad: + # Falls kein Zielpfad ermittelt werden konnte, protokollieren und abbrechen + pe_err = pruef_ergebnis( + ok=False, + meldung="Kein gültiger Speicherort für Verfahrens-DB ermittelt; Pipeline abgebrochen.", + aktion="kein_dateipfad", + kontext={}, + ) + self.pruefmanager.verarbeite(pe_err) + return + + # 2) DataGrabber ausführen + # Erwartung: DataGrabber.run gibt (daten_dict, processed_results) zurück. + # Falls die konkrete Implementierung anders ist, passt dieser Aufruf entsprechend an. + try: + run_result = self.data_grabber.run( + attributes_list=self._attributes_list, + pufferlayer=self._pufferlayer, + zielpfad=zielpfad, + temporaer=(ergebnis.aktion == "temporaer_erzeugen"), + temporaer_erlaubt=True, + ) + except Exception as exc: + pe_err = pruef_ergebnis( + ok=False, + meldung=f"DataGrabber-Fehler: {exc}", + aktion="datenabruf", + kontext={}, + ) + self.pruefmanager.verarbeite(pe_err) + return + + # Normalisiere Rückgabe: unterstütze sowohl None, einzelnes dict oder Tuple + daten_dict = {} + processed_results = [] + if isinstance(run_result, tuple) and len(run_result) >= 2: + daten_dict, processed_results = run_result[0], run_result[1] + elif isinstance(run_result, dict) and "daten" in run_result: + daten_dict = run_result + # processed_results bleiben leer oder werden vom DataGrabber intern protokolliert + else: + # Wenn run() nichts zurückgibt, versuchen wir, auf DataGrabber intern gespeicherte Ergebnisse zuzugreifen + daten_dict = getattr(self.data_grabber, "last_daten_dict", {}) or {} + processed_results = getattr(self.data_grabber, "last_processed_results", []) or [] + + # 3) Datenschreiber: Daten in GPKG schreiben + try: + ds = Datenschreiber(pruefmanager=self.pruefmanager, gpkg_path=zielpfad) + layer_infos = ds.schreibe_Daten(daten_dict=daten_dict, processed_results=processed_results, speicherort=zielpfad) + except Exception as exc: + pe_err = pruef_ergebnis( + ok=False, + meldung=f"Fehler beim Schreiben der Daten: {exc}", + aktion="save_exception", + kontext={}, + ) + self.pruefmanager.verarbeite(pe_err) + return + + # 4) Layer laden und Stile anwenden + try: + ds.lade_Layer(layer_infos) + except Exception as exc: + pe_warn = pruef_ergebnis( + ok=True, + meldung=f"Fehler beim Laden der Layer: {exc}", + aktion="layer_nicht_gefunden", + kontext={}, + ) + self.pruefmanager.verarbeite(pe_warn) + + # 5) Log schreiben + try: + log_path = ds.schreibe_log(processed_results=processed_results, speicherort=zielpfad) + # Optional: zeige Erfolgsmeldung + try: + QMessageBox.information(self, "Pipeline abgeschlossen", f"Pipeline erfolgreich abgeschlossen.\nLog: {log_path}") + except Exception: + pass + except Exception as exc: + pe_warn = pruef_ergebnis( + ok=True, + meldung=f"Log konnte nicht geschrieben werden: {exc}", + aktion="standarddatei_vorschlagen", + kontext={}, + ) + self.pruefmanager.verarbeite(pe_warn) # --------------------------------------------------------- # Raumfilter Callback -- 2.49.1