diff --git a/.abacus.donotdelete b/.abacus.donotdelete index 3c36da4..c759120 100644 --- a/.abacus.donotdelete +++ b/.abacus.donotdelete @@ -1 +1 @@ -gAAAAABpbMkVj3c_kR7OnAA7zo0Sfzw05-3MyOtYVW_kwm6pXuL5RNaqVAIlgDozOuzqrE7RPfvpzW2IRwTvNCMn1AQOXx-EsHqqzRj1WS153NEFBXvkT8g= \ No newline at end of file +gAAAAABpbNgoW0wDTY4Wfcx_kysrik-ut4DH3e21aUvEf-EjB6RYDe13WEnnevBSBwYTb-C3-h6-OWpCcH06tUvBfHwq49AMyWjWq4dk9HC4jTT09rEcv_jeWcxna3JBXk2CylEejknhrDWaWTBF1bZrWdm2cltUZ6M5cS-CU637XvIuTMpa2uAK24S7OSiQjQOKHG_LT-aeqGN-3xjIVlH6lBEVbmLAqqIGJIQ8GhwRPNC2F3gfqRTyz0QB8SaCTGsw55B2VWGIEq3O5Vn-5EyEUPIpjIf-XrJoPk0iMKV3rZatNEt9K4q-fUh206DtFsfrw8H6r7qEy_HPZnP6uC9VjGsf_k1WnEWv6bhfGDytie9VsWfsp_1bLN2KdFQ769e_9fc60JLAoZyedrv5SoNpvEZDdlywuJDAYiCrOkVGoL-P3TCVzGy8qk6B7l_JJ9ta0b1vzxkAFQFYWaGLkV3_1SuCT_afY3vD1ckYui6ugx6-Z5JX2ufqivPtHjWt4CRfCjx0b805jF4yXdMSHVfD3P6Mbsy3sIBjfdaq0Y82Jf3vYU-QOI9CJ35t9p19CoDv67ahlGorwnt3f6KkK49A6lbjoSbX3lIWmpzvOu3aOmz0O8ILqfGrAglyUYx1SH6icJDo9Nj_XDJF9hnjDSbt5hzdu2FtYa2Mf7YAFvTNE4J2fLCxyP6aW-qmHx1Zp92VqtrNhP6IRNt98xfiOKE9cE-vHRD_lHRczhVj_TrAQVkyMnzBXiWpSSmolnFe3wwEWAO8sxFuL4nUWFp-RDpl7KelefzggsaNxMyBQxm-ezXDobiuzHezcH-D8vwxXpsOYkG1F353HEjTF69VZ2KKQ9reCDCFergh6TpUM9Pw9JLCyKin0epw0YcenFpRUfvAkRpwOLDs6eh8kmQQeOaIc2Xtri5Dy_SAOC3QrB_FIrTvgoxHRZmUht109C_MlJ8PJtjpSg8SEVTcRFSlnWlxMEz7GzmkIyeOOC6FkXZtWUIKxYkZ3clPah7OALk_WsM521wAoAO5bOg06cPGpKrSqmYzpKpMs-UGU4TvRLVu-1xL4s14Tzx2PNFQlJuNR2Fz5Mmjj4b03wSXiq6MXO0nOtq_xHQUBzLsOislROhhOl85BDn_AWs-IvwSToOcB2Z3kaJC76RZjZO0meM0Gusac2vJ_lTgz4ykfXOJJm8odUFiPHUS8UwDhpvTQvUyyuHAk98Uo1qf_f6tX1xDzRIzlL_UsuErxJdCcR1TygPPQxazsTsTymVvNiQeug09aJ3vS5aCSwDcdJfBtZ8fqn_dBqMdtZWFQSwuIB8ob8NyrsCrAyUQtFxbIA0pHTYvz_WzgQS-UghJKwb6C7WBtBYlbJ5yic6puv5_MNnu6rkJJQcyHxANWo_YKB5cV9bJrCQtE6_b-Nz7ljMYviMqUnXhAN-M1Pfut_1B6HF2vPQcz6nLUb8d0WAgQTqnvI_qeULN0Ak1hITbjTEPEKqmBm4qI4xTde-6SreFAE8TnOTHf2x0iFuR1wicQ8-TeJeXFEsFBi-N8LAVC8AjAcXAezw7Z_Lp6Z1cUNfTe7fxcfSJ069IGgRG09NOSlZsl8WAvnQGAp708GA0e2APEfFasHnTVUqqF1UoX0mrLumee4L4-NmMWQ4lvqObpLH6x4D6Jcl_Z5Ei_v6Q23r_F2VJDLWFce1LpGlFcziX37JHD0YyOzHyx9CKehItgMGSomAmYJDjPqwF-Wun1Mp74U_sLfXXv5D_cwhPD1NPZnp8-nnMcoOPtWPCrhPgls8rtElpPrPVzTo10MyUNJzKrnkDGKsh8dO3A3Rxo5DhtTeXr0xoG3lnoVcY2Pr7JPEbvH2n31yGW5FJo0pgohRM4sI_aN52lUQuGIGWFqyqn-xJdptaM8g8-zbjJ7ATRkX0hZqpjAq6f-CKmIIGbbPu4-S0rgPTY82bg93NtNZcUq_A3bUIB3jz7RDJTIQuhaHcWj8q2jkKesJ_prczkCeTP-3n8z8rWi_pOvBwr-kHIDxI8eMXQQ-yckOPWu44uW4XGKdEuYIadw0xV-CrIfQjmRuB6rvkOgaZkLALJtY9VLK1P29m1spk4XpoRyUtbGU7L9Gu0BeDYNQ3F3WfhisYQICGUkH_rSsHEsgRTWspCkVJFb7a9HsYLOQgd6zaoMUExMyo6BFOYJJ_G4tnUFJy63MASFUxfXvA5KCa8tWwGzZvjRNvJ7vzoCo45fYWdozsdN7Ll6vHx726vUjtq0pbFJtMVxpBe3J-OVJi5xKz99-LPgG_CQ_4jQFF68alJSm9aYv11Nma8Y3FITWGycfTNR-hLXqCAnE1vylClr2K3f0e6DyYuH2EgV8PnJ2T0CbHkCG-8xUORqMKXzMninON5ZSwcx6fXouaAVEazUbZGh7yZBCAU9O3toJEzSx3mUPfE9NGpxiMQ4tEHKwynDHZ9aodZGhwnMdieu4ui5VinNcomaH36L3yFSWHruurOKwZ6tyntnWKB37zFShgt8h8uIcIANiZEb8oYg5un2VXa5rXpdByFBBYRRqJuAThubJkk4tMkCre3CzoXCBkzEmNXTB0rw6nTh9stGNMYVIa_NgcdQqs7PcWb3cd74Dijzi6NJ0q7Io9Nczp_j-Fr-tS_szCPNyukN1qiwXyybV_mHQAyFDY1SODU-KtSn09vDle5PNtiYH2aRlqQjEh9V-ac6znSP-Sn2eejYbgx6RPogWt6lBR91ESCiG3XCfET7qoT6ub9u66cUZiTgevsg24OpH9qs7OIJkNfkYupaCqJh1zrD7LrDRxQMI9G39Akgus2pG8JTIsB6yC3ndrihDVirBrgnHNk32UaKBOjpj4oS4EvN1ByHLHy8D1oafP6aiSpXAT5YC8I-DGDPKozyaoSiLYw4mwOHOqkb85m4B8NL92v1nNIAjZjgcfRMvymnpDxZhVP1gAKDfSYIfShueui89AWuUvo-WlNTtluuYYCV9yuud74RRLtvsZsUkXEoMgiYOWgT34J1sNG9SiMEXDHpvc1mGeUpnLHXzqAfcfPEPgbwado7duwG-sqGO1q0-aW9XXR4uzZ1Fbrx-Qp-BLnwtINQqa1NrISHC6ohZkKRJvgVS0XbxXMGqNq_wO8WNLz6c9F5R5M-6oiyCw3OoCn38PLQmIWrP37eMSAMlGLro7miyu6PdpGq2aHXflRHV8CvgJ1AaD9FZASCLFwD6aFuNrSMgZ7udMm4IMiEaQFoPBKEJwZCTCh3eGUk0aoAaeURjN-cKA-em_aT2nAF1wb1Pr2hlILGS4f_hEeEgpT23WNkxd3_yftb_Z3bw10ciM0DAoZb9S-FLgxa7qjspLGy2R2f3Ot8mfE8pQyh9hd_AMAhNFn_Z2KcD4qMCayhDXmdAgyaBij5hGYQBngKahMHr-4TXHu8NdazIB9TSYHPgXTXiK2sYsIFyEB_cHPIe7Qe-4ri8r9RSZzwg1o7AmPuKJbimh3gRWEqIIBBvB8oBII7aYLVP1ksyKocpJA7xVC_58SGPILuTWRDWzYWsP53Ic7a8qcJ-VdzIRQ5Nr0Fjf3sfhDW5xk7-aCXNUhYvnUjOYb_hUdV6BdVOBQamaNcKO7H16IxMxGQQG7u16TdGnqyQpNFYLADAyqo3ZWThJW04TkABXIe1Gw1PTZbj9kaAHFKjxDhyYwF8cjZcyQ7jjdecPPdpApEQ8A0tho6zhJo-0j_zn1fIXpphpPqU5e6t4sRZ62UpCl-aqyvgmEG5DBcJ2PNiA80urxADdxrzjmkAOk9a5s1sBYWozxujQjhrcR5EyCLZPhsYAlwCa7-CAxkshbe4QOLlxJrXzkas9vfWMBFJUWezvYR1yH0O1SWYyDqMLr37i5TqPx7gAKRUwz4SAY-sfrTEl10GY2ZiWCgLkU9_FYsi9CVmSn_WisSrregh03sJhs3pls0I77p46e0kGXkrIOTqK8PsizFEWMxfu2E-wxZPzBhMzHKkiJEa0wy7Hc35UcPgv2N1kanlGZSKTETsqQle4JJDUqwsLij7C27u-_pT55-_8ps1Gvd7Ixn2eQH3_k8HKIJSMU2PyDpll5-qJt2TALpl9R9Kclr5nJVygHxjTEW_WmKBMeGW-XrbWhLhr-GXXOhjS0pePsspGCeB4SjpfwnUVzxqsAjFkuJQFfoFQtbif95M1w4qO52tzDKs4WdVvHPgZKBQ1S5q7j72cSDWxllEYJL2kzNDWoBGz8nX8EvsEdGlP_lOLMF08OpcigfFW3BRdcsDvMVvIMR_XA_TQb2h1vbVTakp_0zwt1mJOEz_pZ2-hIWmSuW2udxW9bhKZAm8y-hmvP9qKyvlwhv1gYaCJgnhqIaYWb18IDcNKEV5sjVSGGFk_NlA9nYNDcgv2PJsHlmKOggEY_4FP0-qwV8i4Hi-gwi4YMYQgnuBKCk0yBRFPRsQGIZUDDM7ygCdKsOJbGwcPUNWzGAggJo_u_OI-fFTu9v2cLuQvN3_DurT0ofvQTfJRu_SPEoUm9s23x7N5zTWp2dyVeDoO6XEzfFO31U2m82RYLLZ1m4Gz8SthwZYUsrNv7h4ABj_wOKkt0bkF2DwfuVEJPJmUijfCXfAb7RIQ2Jd_4yN1sVcguiWO9nA-mjUCLgpuFu_-eBUKxJ05YaYp66h1Svdp-pNnSTh0Htv5sCoohCPqkOdjQbDU6z9cLJtV1vTGqO7rGyRKZ56vjK4O2kuFC8s9xecODKewUrjSJyJe50bzR0_Y8zrW6VZOkxvnzdj5UloxM6bOkmLTd_9i1dgFajBp-OWdr6u0IACWEvoz83zXHGAA61MrGy3kMJqS4sD26bwTQGBNk5I4Gou6yDO7RdTlR6dASqZvskIGke9MBDF4sOcRunoPqonNlV7H-GBTLLSPWRF6ILoUvwZIhvd1EpxbijMwNF6HDx4XvEERqJgUsti6VNorES1g9ySXUjwccFkrm0asMq2ciTE0HzJlY-HgmuSXJIWcG5F6u6Oi5oqPb5HLHNZS3kBw8PZYL4iSQqwqTQ9KX1o54yqXMtgMvR2E_uAx3dvcfKddLDitil2QCD398im40qIAyFmXUbLoiNYB4HXxgJuZvYEH5g8SHuIzjPXhSBAnGRhAtIQ3m8zh5ujUu8VXS2TXMqZMA7a5wYj4YmlluiZpjDyZ69ZOVtrwp7neXDz3Y1qVMLNMbpYDA09VXQ0PCHZAj69C9JVE6l_dFEGjiZRilZr2lKM6BINx8HOu1g7vNzZSEg60Tkq3s1jRNJj-AqxEDHJ_IegOho9odeNgQ3uJ3BAAWQvhk5zsAV2qQ76ZrFIZD1M-J8psnXNH9Kb0YIEUZULnoQnaNmbamMVPlE6aUHhOq3N3AnAA4-sbUx7JkuGUamomxuweFzFyTA16ZqBFY4ASxLCe31Lwpi0ZwVzZXPWkdhCpgT1rvctIMsF77krIK3cRTOu9LlrHSuUbWRU8TqI-KvJHPmsVNLBSsFjTdaQMmpmcnpqN0HOrVjjZZHH5UHWUUYS0rOTTmHKe89i13HJwaly7301N5cfHnySRtys_EK-ClFTkyt-aIy51o_qnYpttrApvS0JCNtzC0OHXEbyYeFB_kvI9A7dW26eXHkWO5JLtxFalj7jxL8iCOVg31ZAWw8nMa6KpIYiCMpu2vcns8qEB0O6ejFVtWq2fSJRvho8uSQ4PVQe3swFhcEvCBWGqXRPLw7nYgh0TdABrva_6nhQ28CisgpHIbxBW49zOQg3Q0w-L-DXG6p54HKGMHlEG6YWmN8VRmvKuoBLNU5ZwBxDfeExIOpMkXCjmQXif2BleP0OdcUtcN-VqzYcBjtKJqMVAGj7gj42ZQlA4VSB9YQlYqBXaDqqKD19nDseHjKQJktzkuiL5QuPO4GfhSvIJZyhLNAXbe_xAGSNp4eXFaCML4CreyISbuqN_YOtC_5FSAXteM6m5Bq7H1yqp9m0Q8RkKSMuv8k2MInh-UNl6ooCzjAkvrNLezJmPEAEYJD36yHZr7noUqVY1hyUoKGwCCDXggCvWKZhrbrRBPx3gzNvtNjzSFr-Mc43IQPGE_JFdpMQfx55Y5SdOt9JettGnAv9NNdX1ZssDug698iSFeqdD-BBF1wNt387lorA0KZFV9q_D7yzCDJY0YR4IaUVk6dOoYZJ-gU714vHjosys1CEZ6ZgIxVO7kE9hmK2kLWxuzCWkyERiZmFWHw39rlygY30nfAOg5RaEFjpAyQCdPYNhZrdx42N41AC1Oh-bgnaN6DQLU3T7iCHgqL7kTr4h-v684T5oZmMi8VGvjcw5v6p7ZGQLWbnKb7PP_rVRfXElAUafVj_HBK03Nt-t6zR6KqGJ-GVq7KYgCrw1RQINDBcBsaNxXxbmsE_11Y9A3tz2c_Uj4_8S7bhCUTNTWXhZ0BWmqQmwL6j5Kb2f1n2zpYrwtm4kQGzQ8dfDCTslFBWR2hO22RM8hYFWonDvjJJ4Sr9p1a5SSne-VAb2jDhvZCbrjxBGUgV9qaIQDbbnwzTVVruuiJcpBATQRUoOdB67Xs3B1EZTlF8VFi1C_Efiy55j2RzOlVj15x8D30kbYMjzTmsV4R4w5bJxcqQmdG4my2jWX6mF3ldQLCPs83YEAUb1Yo-YdDr5gq1hGdMg9KcWGF0EY6CcFQzBrWp8eCCAgBEFJxIOtwK0bkUIFSW127tMq5rluAcp-nddekMBeyi2G0cAwYBpz4RxyTepGDK5AHXmmfV7Fvvom7FGUwPcsAgEpbWFpULC9VHNYqHL-w-DlyDnMRYkqDzikugenbhXsqjeXMi_1LYusLnQp2ld-9jhXUAxAislANFztd59JfnQyBExnmtrYI3LTthxcOw7cLoJ_z23oXvMgSx-GHdUYFc_zDKp3tbx375JtQ2ufIvHkEqGycF6_GGKZ2653w3oZYVk7goVVigEjxPwpwt4CBPlTB8bBPo5K2JMCzm49zALfqPsRYWa6zl6nKRgLNf1rOp67PHLIdDbjVMJBRsisL5IAI8TVNwP2jtlzJq05b-eYYYKRfmTR_Sxkczb3fxZ4bYnHcGCDejizMgSGbxsSJmPc-boUrTRo8C7RPekMmm3FZ3AgFzJpzD12j4owlEROgw_HlVe0_V8w_nJVEYbjT206H_zC4poGqNjUgiqCYbLgrdRy_ahYNI65NwXYMpCA0zKZWPxzl6CIrfBBmWKUyFy8c-jPsMBwckeVMJ3NefQx80jYgAdh0lWcgOwYtq5BtEE-N5T2Jd2CQP-Rb3cEs61NR88NTM_0WYwUfc1uWZgYaynt6AhoUYuYx8ZRUAUSC77c15wy9Ifv6xLqSKBrWXUTvfj-4zatxV87Wa0wGPHnaCna8hY6P8bbNRIjlWZecRC107UAPfYwlDSAjGiIg32fwC4bOu4l1vxLeBgDQpcdmN57QN6dcLT16nHD4q5ZQbV-DYdomvY3VnDJxeTk5uqwq55o7krUuFsOHkUMbJl9Yeyr6GKdjnHB2wO6CGdBgQZOSNXt4uPzPnJkQqrKnQJrQ6-Bq3B1ugpGEtzuD4viy1v-_ySQR5psSW2lI2_iZtf3cqwJ7zBG87c0B8Cwv7jr3blew-JkEkpj5w8VB3LVb6rNF5HQ97dguFd1s_5PR6-R937KI6V3F7ahnnoS7BXUdUhpXQSxckwzxVwGudAnFtYuB6X1z5r-dTGVUbs7EHBnWtnQckGS2DM0derd7RL5N9LjJIxzWUg1h_EClYEwW2m8VGqHz-Lnaz9IIkQFrJ07nhK9lTt_77H17E-LAv2VmeK7sLDvK0JmqcbfgCDQo5GR2h4mE5cEoix2j_MoaHZxtOGXJVMOJGhSSrk7h5DTb3nw7Rh0bBxdkPrEDb4CjiddA4ctSopzUnwh8Eg2AJjXyQBWdZaLElwL4Y15XuHK03mhgxuz6_WcqUGTxkko_IeGF62Ffy3ivyq4KU-gAQ5h2x2fLma4G-J1wJfB69qJtV9AzDmkfimU_QjaQpjaNKA3rF9J3YWSoMmFRFT-uJet9Kkz6vrxCOChQjQsWIWKccXD1U6qPaBSX2Jd_8wJV6PJGcQo0PwTmVvmWvHDmKiGnHYeiE_gnrlkZV4FQCZ2F_GMmFKfNrqe0oE5Yybxv1JLIdLOIX2ErNKsvca_qcdKI8rdSqBpCsa0ldz1o0F1bIZ8wYc0TFQgmMsBts1LEtP0ux28W9D7GhgXUC27PZuE4CHPq3dSJSa0i_iWu0YrBvBcdVsoD9UHzddYUxCVAYbyWxn0gBJudBCfWBeqgBxdFty7W5nbRkat92SzHLddvkVXPey-6IQYNU76RPDANQJJozwKtzWVOUAGFMcGA6WMFm4U7amgnN49tebhwqt79G0ZkFUzS9fnOWQd6S4wKMYdrMgNoqbdEJptTs1KwIpJ4Zs9aHnz9-eQ7zB4h0sNckMtAVPb5TnELExbJfGhER_vcRfbsnn3N4y8Fa2ExbjhQAFOInkwgelWTcMolmiUhLxoTkZ0S1V8uNki_KaUPtcrw_gKXi6ZV1Qj8uXO9lUSpRxLbKlfhpPnvXQUjw5fbaeQOylJCHMgzTERaPhhSoigLpS3y-cOGLo-u5mLapSzCtCNgLN_qsYqPu5CAg6Yy2TZhP3oDvdyvUQ8_P2gvCJ7mfhbVjwATdPfMCQnLmHsI9zfu-oo3W7cLRevKXtXvZe3FafNJgNLf0F67liucJD55LLvteBeWBEBDZQk9scYUCBy1HbunkW6kDFapyIDOkmIxpq-xpXzMiZhGuezMf98zchcbC6rkdC5OdRvaS05j81nmtu-NnHvXdoajKRvdsO4rexA1bp8eAeweXMQT-l2lYEMU17tN3ZRlPQxGNAO5mm5pRQlwG5p66rlNZ9t53jlSdL33Tf8CIb_fhDa8aMEDHHVXQbTeuMK5kufHoLx5ivo4l0jzP3XcC1IrzEFbQXJKrmFBnYXarRSFFlaydc_vhvr7a-F6EnMsMmEqH3NQlaE3JVMKYNgeK6Ef2gKaSvyZ1el1Xq4tFQTOdbxJEhFXTHfDTeAlresNR5rGkK2ErFpW_5S0ROp3Ddtefs8rccFZ80mwQoNSN9XrNfpS_NVnR7X1rbNEn7gtI4WXD2VkmocxVRz3jWmnJYnryrMC6pXLypGrvg45m5OoofqeyCoDC_FDSgipGcj2iCkbBXdsbWSg9ayS3mm1K_4BTCW41N9ydCrD-qVruvCnBILrPhnj0U_Fp7XHuHZR6pqSaybmIhNg0H6jWNgsk2QE4R7SWI7wMU-olQL_VIusqMbdS_2XdiKD3U025lIKgG_PSqMleDXrCbJ1MqoaTK4tuGpBUuOQDt4GpbklOBdtW_XEy4Z3C1wCPq56Sp1n1SvRkr2lWjEMgNEY4rwLae0i4spuGi6T1VRMYHuQmllK4Ksx9LmpN-J_0fgx4hq5Mk2Rb7gy8DmN1Myr9geoBz9SnHURzKDYJkvckR-BbwKQZZLtZBKw8AJlYZ4nGQhYJfKzoBShwJQc9UCyESz1Y3E5Hd-E41iqW65M3N2BqkjJQN6ov-aDzXKh3IXDOIdhxEG9T_Z_VI8ELaaxLKgIRi1PFA3k5AfY-bZ9xcofhu2B0Irgs0vxQ8H7mF3xK-yXs-iKdziAJ9G7KoF3oK2p4M4vslGMZmw-RrLBeUzHZBu185NKMgjM5guApe5AP64_QWHjvfWfJalZwy3_TZcIsLKRMP_ge6vumslqFkDxJFOaY1n8RxguWgqBYmkVtHkJlqihKDdIDsyycCzzziyxKqyAY-mQjIzpEVX8ybYJ-LGLiJ88x2wgzcHWOJPu7-lAtYvPCP2P4dIhP9XMVNh-WT1bJSxpRJEWgOsiQH9ijrGkoM6VAzMsU4VPLiJViNu9FEFpSRau_rossA52rIL-6r9a0KsUMHwasYwhTnqZWtXLmMLTq8rKYNmrc0b5W_4DH4GFnNGix8CGvCcY6S0Sl_AlIK1g63yUeAc3vAnyz7TABMQ0Gg1ThR0ytMstDnftWQ_SXJyPHNvZfIqWuw_XFS72eodLMzPB7OcCp5ycfUl2ZfEBCURJaAxGLfeFdtQf_U8nOzanXaPSz_hVerkEy7BEVrgdgp6rSBECb-U1SM3CoBEz91XIEvgUsdLa1ZG5Whl4chIcpPvg4dyQAnJ6ikkwyuOsQ4KB1Qjo_GN3kCdbNjkQHqEIzXtOQDdNHgUuorqXT6YJVYMbkZKWnkggsb97u-FoGiigi8NkYCK_qegLOkUdn9wR53VnNbQbpQklu_-iGN6Locfi5uHRwnQ-s9MblK3iji89Ax9bkBuiXh85wTZHdi_Xktk_g_lCCU0VSYGkVH6zB1VMffOkRU40erZYoyAB1o5lNl0-YzgfnjdLybxf3-oQTr4yUgb4-qLUYBp0NNs_vre62pNsW_kTnlNaWDEpcx0rmLc9ax3OylUE5S9_amOyK2E29yl0dVxg6aK_efHxiaUZcPpabNL0DKyoggdoyxtWH5dmdtEfpgKnjtMEphcxa_lVGF4cxDZKry_MEaIf6R__IcYvZvyknPYaiE_WKV2rJbf8J2MyRLLL2AlDefuhXazyaIkPxLLJuqiBVTU87STwZuId2sghhUSrqQ5gPuqFlSbF178cLecHP6gajFYhZcAa93xa9sKLXULYGlq0vaAS_YSMzAIiBV_H6lIHHpBB94-GgzpqhrFDE9JWmggC1EUY5-nqMpeZoRx1gVF4xVF0DFciggnjoM_fU4hA5QHvOrvweTPaaV380btF-x_RJ1VSlPJcLv5hSuQCc8tYppmHq1yDEHt5Oq_0u2u1th6MQWtB1n3718iR0zsnGcsUc0ZXk-tzVim5kSgDqnSEHB3iP9D54udZ3hqj43oUjKnn4GRypoB-UxL5oKkMocp6ZJkS-WQmj6_2xjk00ceoUzNqceFTJAwD3XIVKKWqt0JUQVqO47CnNdYLWR85jHzt8fumb-mYbFOv3V_C7E-KnTnGMFZt2wDsKi75gpYDkuHFbWbXPYywPAWH9BpFI2KBW5GnztbZDmxzn52c6xHiTwoRwBUFZvvefk3FwFQ_puhit83U1v_7ECH45UKlLe9J3J1nQaLskzVM2OgQI8xLs2x0sg_ClqPgR4qlLiSciBHhTFX-hXUOlNaVsMoflgjboSiaWk4_cptQO_-Wuja32aWNjbc_MCeS5BXbn1YkOpM-npVFY4gS_VGfvCjjl3lpVs7OejrH0eVLb0K2uVyDzyYIKZX4cEBg7LzZZA7TsXQ28Dup01RDNFSjpdrzAiE6wlJHeBjtlnqzF1xkpW1eInuAa_SJUZLRWLXUqPQKVy02ITUh9wO-Z3JLTE2EuVN2NpQEobx9LX55-S2kiqPtx-H1ZENXckgFkzl8TRNx28DKIviJNf1gk13bZSgiCV3yI_yKO-VDKHoknpQ-5z-6L5a0vI9OBM1ix4pKzs9vHIlpj4dorfbluGq6drqET5UpYRRhfalbnoWxdmp57wIDtuLsAFMLxojwgP0hjiKMTzFfH5WiISfNulB8PTwKmozZExWaW_c02_Ae4Tth_Vwmgd6BuECBl4xAgGOdRR9kNUiOWCXhcKeQgbhDvdaoGZACJ15ThWaIkurCPb7poFG67uNUZ9RkPq_uB_Abay05IoN3hNRcK3pQMn0YK453u4yChUp--v9-_H1d4PR_iiz4_wLxaaQy_6E6wN3dcdb1V2ot1AMeDyUi1v2pRIaj39r8X0Nh-HSSqZfGNlzwrp1tVn5q2BblLBiRuumfGBeakxF9q_QYAAC--yojhJxmg49fq23yyQ34ivGArRirM3KwLRgIgk9HnU5im02-3Xjg23FHN09arFnsRO5alobOZdHVIICtw9xpiHyoED0Kl3rQoamsNZ1o6EWWNV1EV89SjsOCD63NJHSCoo4k91vD3Kj12QoYT9lekP9JarBmz1LExBZ08eriXt28E9L9w-J8L_7OgZYNvYd-7IpJ9xP24pTZib2G8SgOGR8iUjv4Dt1PpcZnEsGuNXq9xU6fDt7o1iHAnS6TwzeHdhyQxc2HwOomTmZxrpNVcE_TTiWi7B4KfA_iJ0_1UELgskqxt5VWZzy6y2GQboiTLMB2Rjg9ww9peXuagTtBElEotHq9KgWHEunn0Hiz8OCYLUU6ZmnRjIymoe15RnxGNulaJVOCP-_0fKzU_ApDoXr850XEWnCaCX7K6rpkvLOO7g5AwaM5Jw4La7wBIhZyBGPMyXf-NqVT-1Xm4sMfJXl0y833TObJBDemiTzo9kxpHG6A8bEKp6_5zTuk1xD7diK1P39NHsXhi7VWtTCYTcxORZEYOfEs1BXEKvpXuuopYlTX-D97KUyASKGtRg39SN_ojtFpdIp5uFJLQdb5LoMzkWN60BMFfvE7n55WeLjKggHjnkup2SMyAoWslqPWeo31P3zzkpXV7lGaLoSrFOMZJdvNgd5EMDc0KsrNJZTwUCfuBdMNKnwAaRhi7TKEvdcLXW3dhe7Xz2ihM0I53pZd3_J8VYLTX6DRH6YMK1Sbz3sxrqxVvzpXbLZ8UIk9ZbobAlnD8uQZGwFqZX_k_CUGJMrR2Rzib5N-LRy1KL9-JrpKk2txwnU3m5i_um7POCtI8MsAoRr-EPJ1WkJrm4UFupPTfZ1u6AbS95tdoH_EUQA5FXLbvO4J7XebXsVD4ou0kJtnQyQTUYEhtQ_GBHBFureO_GdbhKJ7beeQYBSR96dXA1BKOq0rENyXnBwDw746DsQiulNOiQCnvqPyHaezayKWHv1mYCY3PaYqk_upbEG215zId0CWaXulNVWREvQnTlmWlgqgzK_5wLZPHqgxbBH84qu1JrlDb5zprpEooAsnxDeqRPb-xJABKbzSZYovhoPmchl4chOciVYNBtatUwG1tulnfn3nGswUi5aSes2zXk7I--Ayr-FepL_uIEKKfEwGT54lVHv9lEh_vDW46E37jjSe8FvQz9VYuOWMKQMY7wFiz_NXDAotql3AZvvdcnQwK5wXcylx-TXrVLXQmCu_fgyOCNfPMfxjVjtCNJ-Sy6RpI-raiMYt9T4sN_gPTILgXxKsa8wc7Gm8mHAjH-4fp12Ssrbo-giDtNk0C1WBsfo00q2puDYvURxVRuvWhfcaJls4dhH2YkjqgI5x8QaKe4ymOCGgIqUUSN3P8VlnN39UXUYTQe7cNvbmSUqpSq5xB1NRVfb7BaKKwlUT26P8om9YnhKLL3iKrsQL1Rpk-mWjcNhwm3RkhFuUGO0seMGvafD_WBdd4l7WT8D6EIisdbWD9lxefz_WC_FREkRTOOVY6llSnlgnrZdlLqwilvc0ZCo62FVarHk6XZBfGMC_Tt02DfhnKmOqE8uBSRUZv7is5fF1M67imBx-O2OCWQUtjF1gfUtBxGsXfOlHOZSUv76A34LJxzQSkbkHPlMKaVf2e9IoxmH1Hs1khJkRHsCBHdJeInktI77Dp9lxId1UfPkdTJ1YdFvEkdHhthlvSMufqk1gqq_AgIG6ltyByU4r1TmJQnIRDXRSKgiNAhIB3Zw62S9HluUR4o_BMWtL645SsvZA3aIOxKrfdaWhTgu-q8dLtjX-gU9WAwswe0pOaLomYbfM4CRugREcEe3c0kIto3D66iZFBQ6JEGgGQNIiu-YMZXoiMYf-5bHIRPWpbLz5Nb_Zbq21BQBysz3Csqx0cOXK9WfGdyjh3Ja6e_GPa9X51mMffwzNNK5kSmw_jHk34O55eH0aCZysly3c4swIg3fcOB0pCqLGiKWpxBJv54w0WA9_fPjMLtTZWZjEX-8WUvmze6gv_YhqPHZEAd8duXTF3IzFOhvBfihldSXvzmz0njTpyUyuvvYj7zWYNVctjGFZUrGOCmivK7FojgWyc8wKkBRHbsIRa0s1FQIh0ImGef8P0wlDsMyq5pIRQIuTdauBxNZSsyLRV_16S9ow79mQ3oXUfTOtrF99WZ_vcfFhIZWhJxPd3s-sFBiNgTJZ7ygMWgnbCCsBkc2rznND0Gxhqy1PQu2TvB7gIhicYkQkqxTsn_y0GfVYPL5RSG0-OmbTj55MSEu-498bsbzK43dz4S112JVNsS_whMNoWly3rmoYFKdjrsySebkE_w3Zg27nH-cLG2udHUK1X2RxTva5M4azErgzoBC39gxTNC8_z0eekneqY8Ju_AtnHZnP8Wj_MhOXQkj4Dx1-jch12DIn8kY6eXWNVsXhcSL3fEbyJW4ckeNcLyPrF1ts6AVaxvZ14NaJTSlwys_mu3tThj7WZsnmPZ6bgl7WeN2DTygd9lrY7G2NUKpwI5rHVkqo-i_jAh8-lm2tz-JHzewrfu2PPe_y55NMjnOiLgTnc1P0g80MRKpL1-1Ec7TRbz6xqx6RKUlOg1NEOeAC-eVGJqhE7hQkY9Ct0R-2anqBrLJI_gUtrLZdFiYQ_OsH3-KJbO5zkIXYb8QgzpC1BE3Evu2mmgUH_YjiRm0R7k-CTg0CsF9cCv8UJWd8G93dbVzQL7bI41KZ5XdjfvJwYUs1GnVE130uZERt15N5eINQ2aqaWLTJ9f3LJ1Tv862h_ptQy_LaEU_QZ0bQOVHpu20_D5mPcozraf5dqMegkB7m1zpcesjZD6w_IlTmE9gsjTl-tBoZM2CJZOZUtJXYTF1kTn2ifPukYUKZQPcuipOUc_sJRurxmlHVki_61rf5l21py3J7G5ToIcAmtgC8j7rB80vVYTg_P0GDWaTZeq2FFxfDR81yzpUt59HsTKkbBw6zsI1mjXgGlP5F6IpZKXr-2eIQulpOs1sJkSDHD4R1UgKT2BtxQ2qR3d8VGPwCXZqgjW0TkXvuknp_H0QQ_YOT7R_kNApGzzzOuZFkJR3xoslXUlxdNzlNGDegDV6b3X6GmSnSJVnN48P66KRh0b30dTGjEJpJfqi3NoU9TSfWExY6rI4VCKRBgNPTp0GOAF1pfVP0FH5S_FicJCmPq38SlymHHk6cNjQiwqcC1ySbu6GXsvM_0JD6H8YIzIZa1TODNGK9hbNN0_p9qZJNdR6YYf1rXWFywFmSf7ivjjqz4dIxiaouuRqgQfusZ-22uhJKdI7YwX3JSOTnpTNiRpE5mWSk-DklIxbj5cYyRg9l0nTWUk6ZGWubPW3XMxjWG39IcAenxo3QrwKHG81mOOjLtNwXfWY0JSBLuQvYxo8_m-Jh9iV0TgjGsE142q2TJKWJ5FPejMD0eZFAm0TXa76_5gtKJkVzzXrjYKVhxRPe42beospqIpWRV9ZGmXxZTpJ5dQDyW4Ps6NE8vPeddq0oNYiAZuF41cJX0IErp89onBGcNxNfS5oGa5jfV95GcnBOEGOZb-jp_aKUBe-EdT0uVbDOxGA7CFE8Jwhag9l08Kcq-0fKVv89WOcZdQPPzXTkOaYb6nDXjpEF2Prl3ZdVPoRzh0miH_miojeLBeUIHax9-BHAN_xSaAXSld14zhau2EyL8lEZ4563EgnLwbiF1vFDxGGghFe5VJuBpKthWai2Cl10dwJnvYNlA0Lj-FYtyBC6tMlD9aTRPWQIELy2H4Kg_wkAT_CsHYCDaCn_9UIyC_x1FNKAss6hvVz-7iWjdDQQe8KmUksKn5GQpIeqpJOlUmyyAQmBO1ZK2M5RlatQQySL1oHLhT5m-oabdRorIU4RPZNWebV0y2j_SO4SLAngT-Qlmf8bl1i962T8h3PfTFG8vnU-ohf3Tg5ERP58sqfynjatoT4vt8sBo-h7NPUuKG8Efmws7QY-TSRhPhDYU_AgOB5uxTk_7wz_obce6Rpseurb_0bYnYpLIfTBBL3anJyeoWeVdZTH5A7IffMvWymJmKPyRExbyQk9UXwzEgV1e3MuFuWFSPNQY2hlQGLNELnTAzC5G0GC5SVJ8spFcEtGCi9xRJOYnBPLgHUCT5NaLsQjaUKCJ5Yl42BWFbYp1WL1rbygiX6zYOyqQ-PxmqjbULyR_3-rmw45BBnIWLJFxyiQsGqQsRswf0ESrOz0L7SFvGhm_vYO2LO4I8O1rXN5xSjr2OvYxkwPaGQk2ojFDqQC6dLDIdlXJPlz1m7uFiaSKiWhqkmVcgCb-tc290KoskqmJhtJS8IGQBKxlwe9b1nfPCs2vDG3WH-0rmZJLC0BMAtT4KUjczFyvudEI3gmsRoJJ8uCNH_7udCDVcorTnG_a9kPixzCsioKb8FSKxEaxcZgHsaMBo8nXUsYpoJTYtaTMD6m0B2sXV8zB8ENZeYMXQ7bci9eXlGPRsnSbpnKxfDXoLuk9TXg1PvHRNn4MX1qHF6lm51-km0rw3zoikpStJQBUMvCknAMO-Iu4vNI2x4pLivelyW9m48A22vEGQCg69W7SatQEys534yc5m4htKwxKQLo1IX3v-5BYh4lQsdlXHveGhGaBXeK1qHMVNN4xFtzT1HJv1cCkKhcw05QJVTUZUDDe9c1SEHnspiBSN6lvSJB6eV7-dlS_0MXdZY_JZckZyIMO68L9cx5OAXl_gtVy9CT2y7o4DoVeN1M6S57SNb-qM-BM4K41haeXAShjh6_b_icCK3gyAkPw_YOo31rSm2jKcEAWYJbDYcJNmq4ihznbPeASGA1zxTVnsp5zLzctNtz6r5xKKg6bnScLcGFZsGBbOTOmXQZ5t1unhvd4IT8u0EJqhLuY3rvgehYUc7iEETlZTS_Yu9pXYYA1i0OvLhW0GDTsEZkO1ba3vWoWmvlLq9_cBb1hwCplHcvdcko1s09YxDOROiJnQ_CWWgGTNDrZYxxv-0GCgftWOJ8PNWRVKe6vRe4ovQ9yFBo_0JqJf2KFYBZ3P5i98CSEYGnCbUKEbO8wihpjTcW2DZ94MEj5iXndkhZAYTap9Q--sK-fWc3V1y-tpbEn8w2jwhX3QVqB9NX9jrRH4aCOMRaHawBp2wQKOvaifraodmmD27RChqjQQeN8bACncCzLXGJ8udqM1GZIlX89RZH6cyfPbSmx_PCnV9_OtGw65e6A4n02NY-t8xWKna0Fmj3SYh54TpBjTOmcZKQrjsOUZBmBo_f6wsSMYKRCfMmPGNA0HKlKggrRfSSana11EvgVeWa6wN0r_SDoEGz1s4dRfiNWOIJ3jVVS54Ryj_ikqubyQW8BGFH_4VBoRUfLANfJt0NQeM4B6i4y7pyq2zK4jeQsXL2T3Cn0ZsWBzLDC4a3N1IKKujShTGjc0XvpIk_LrjYHpjXrrPSl6G9kfV_uGJnj8vhLUTr9IjtX80IOYqp1IYtRvXTY7Y4B34S_Y7yypgkXHt3b8md9AJmJOMz2RebABrMX1M7OKUleJRDOMZy7s-ByoxRX2Y47NK8HAU4H8zcfeGbSaO9_7Ld_kfaHbPl2NNoL1SOwlI5_CbLhOe1de07eso0Q8g3mVCRWKpMxehTjxBOCHz_xJpy6uHL0hM8tnNXn48KXcddMWdMV7uXz5Y_froxF_74BdIUeV60BvxOECEXeeRFRdUx1YUfC2aFaa_EDiuiDlY3VdBRIL1nr0ZMXEnob9no-SZBlXGjtC78wnwpQ_ke9hp8s-ZqGTevYZ1m6bLtBO22mRfm-eksj9JdSM7nNPoueWC15e-YPJpxGMoXKFsRRs7iw4u76dYEp0LUO2WjUU5avdhd0UQjLpLty09tl_jR--gSj569mqbBnPwtf4N_kRLr-Kej-DmALbvTXC3vECNNXTKII8ZYsQyA-Wipyra3gyknDL-ekYABJAPeCzcBpvQJ9LTAtA_-SMfRv0Xz9FxnCTW-qkFEYJ2GCbkPz2aw4Pa4pP04C7Nv2NCWL9RHcsNZJaypFJT0fJPEbW_EW50Rb6CI4RmI5W_l1FyOtx4QitbfMQGMkxqilsiSvjNyAOlEoHoaQ6goZaxQPa5l1f-iAm31JwtUnYvcqWeF1inoGxFte7_cJdv1TlcapsWD_vv7A6zk0R6lTB3z_ThFLkzuXgRgUVGKjkmDkgotSAcnfiaEqTdkQ-Rp_xXL66G7jDOsGX3eJyMvX_k3ZPSICDu80uJ2Fb4yqxOJHWIaoKi_B_3a2A_1DPAvjw0kdyHvak8WHSFV2QYBJ-S54jcz7emgxJprGZJB8O9J63i8-G5oN0kmxoP_K-Fg9Yr4ep0wSgcMZ2YHyoR18-tRCNQFbC6uL6O5go9loT-DU2_WUKk8VmPjsPR0-syQT823Xsvc72M0e2n3p4oRQXyIruusWmWnz8aZUWW4JL1CA2F9xI764LFJNlulMHLylhujqEb4zFJukLOCnxcuJpiWBZPXttgXeAcO1hd5JiK7c6DUcdU1qRJZYkQ3xQGd7xdBglz0QV_xnn7NnHWJzH-tPI1b6EAZ62q9WcOu8P5VMuELY5M9k0d7ovJf8pO-HU73xToUvLwNf9TtCyfultnSoD5HoD0A9hXI46kYbF5P-lbfE6aR1cltRatvUAgTiPncRbRe-0isl_927ajXQmF4aJGIX0Crdb7dLBfB0E-PGLRyNjtIp6pXpM8ZWv_HwWy6ZpACAp0jPLBTbNcyzKLnPNaPKLN40nmc8NYiG_dmeQ8Z_sasOJdhvePFnG3101zfWI1YdUMHbqlDpNtDq0m08f7Uk1DLYuWCtiII151SZpvwv4uMar4MPGphLDtdiL0HCTqOtMSANRMkBCoHfZIUaIobztAXplVX-bq_NnDF4sOZNJf-HVvhpY275gyjYrF-MHSMNE7yKk36iWABGCRCDChXWZwACorhbXX-08o83LNciaW_vpOl1_QTcA7NyQ_mM7b6bRqeIeSc85zYE_QFaMe0DmRG-34jm3Zyt6XW43lB6csfYqYFVewD-Q1YihEQVSE2Tk4VGcfgu6Hg9AOYT9pWFtZKj498_WI-VVznfUutQe77BgfdS30oEYUGCMwqfuJzcWDbvoGIXxQbaAnYgwmf1A5AjVNhxddYgKe9_ON_8ZBSu36knh_8lTMofcpUs3X5tQyjeS0L4mGI3u4sjbD61K2IJMmXGF22peV7ds7MMwom7p9fIxtdWH9Gx8UT7Cdm2NGjZuISFSSqkeBS4tYLm3D-WsBvmVrJ58kVfdCLM5Z79xf7OSv0ZRusc7KJNAM7XQSICmAbIWd2bS8ZTj9C92uoCkQL00msgLs8oLrxRu-MgPd2U01pmuwIzc1drLkps-D7nSwig7o9gj5WSRFp0RwCr8KvqHD86qfRuP3-Cnxjaj-4aJKiSo2TgbXbpIZqgNu5vcgnOFpY5mZcwtb3JC2nasFoGdLQQxK_wr0x6ZbH32Wst2Ob3P9cSpqMCl-KoO3nuX7NefyYYPo_142NB0eAU7al6mptN7OJuFnOITQ7OYmDUBOKYvTrCZciimbus3fKJ3HyCDKam69X8tyX5mPM6Wjl3foaoF1_VungiCEambEj-qKekUa0uFgcfNNq_LH_wgoex_g_fTZ9zJw95fgMzF_YB62S8pxhRBPdFJuRebG9UtZg4c9a0XWJL7Tp486gDBEk1Byp9MzcjKafCfqfXkcxWT0ZuCcKcrLgwprdDqDuOrt38hql9m1qveKSiD4oj0lPtD9r-4Fhicd1fGJrTefMFbuE2y30cJAhAwS0yqYlGwaFJLtks4UHwdQRGrtu47B2UoX-hCNNhpBJ5rDkZ-hbUtNtFh-d5QNVa5aHLZ3tLN5M36lNamKhRTOwL6j3NXv889EsKagXghWEHxf4qsIbadzVwJu5UAm0V3JlS0EfpmvsC6XZtIiVjUtePYoG937QaBOkdIih_Q2jk8mJ_LFiHngnz1WOG64latTQjU6kW_Hf1pUREfEWB8PEBmfBP3LvX5D6HnKfOgVVaTwSe0th3f19HytHpGZrNKxMN28WT6zvq_bw1sU9i1bM8byqcsYpfHcIGZF3IVAJKZdYu8IWG-jggsUGn5vGnQ7vVYJBuwBT_iKWPGPUTiBpkZvp1Jov5nuFTq3Lsmew3HFja5Sf0UyLQFwBXfmboMOJYV74djdJ69YSZT0tZxTgds8cNNmIk7vt2aLqcyZCXbvHOxXMzkp3eLulh5wEtGovyMC3OWhFoGySkFb0YZeyWhwi5IN3C_Xrowp9dJC3MCsToyxnZR4aRIAmwxqQ28VaAVTirvVs_PTOUYDIHtZ3Tmt6AM7tZREWVegcJaxaxmRnlzxm_4nH8M3i-DE7Yii-j969OWxtichDTmmtn7gUtP9JerHPBUeMM6N_kQH1Wi8PfaTCQdaX0nqVHAK-B8I591Aos_i-HRn4oyG3eN-LPwWTFmfLy9UxqsVSag7Mmxqib8HjAoNIGcDUxldT8HMeViA4S8Gf7FlNivHwvMitoSRhK0eOl7khy3XRBuXMeI7cXD6zPa0pmoxssOuOMgj-xNrF2vT_Ui2aNgrNXIksUSV8E8EdxzkFYszepq-VEOQe6B4LOyLWQD92pfUbKv95exM6sIbcMS8iib6RFdiuDbyLYJuCIU6qtXYVTPeVFquCb9gJJV2j6lFfoqI9F2oYbS1lZDGUT637wH_PK3PptZFJJ3goLwALNKD4ssXkYLQBzjjPs9Dd1zW2IP1A5Ikkkb2W_thIqMoDILN2xPb3ZGvFL4p4vjs7jPpSMRe71zgV-H6YNEf_YjHSyEogNQCaxgTNGAyjZbppoK1Pn2Sv68Lmb_oIEbF9wCBhjzVtt8gKjHS9mkl2VLv1-D3ME0EeUjvVIKNuB44cvvo4lBi6FYsJ3kYKcx_2EcYgF15tYdO709fkfGkOVZa85Qv9jZWBUJWt0A2IPfz2Vu0Vh-QhvqSMheo8iPxTciRyf9TUOSNsFuFqTH8Wc-cNZjIy9PwM7y5rFXUJoI-2kOkLrbEc2zaaBELYoQRX1goW1Q2C8TD-AFRaKBx6LFPv-5SXbMfmMRrzWd7NBKCeryBFA6UoYWZ2PJSdhddtH_Kg9WnWblklia8QnLJtzYoicqwUqW-Z5oLCeHwMMUhQohk1yBSYkP6bWbQ6P3uwPo0fg9FOIQyP0tHB1tvb-fRSfvlv9O6oCI9TrOPcuQu7X6F0xHYHjnVb-qiDSujkFyzIVHWmBRaaCBi3W51PXfClGY-tEvQvjr8BfoOKuYx1b2hWsYn-e5fZUv3WREZUCM-ziFm2quciEq1Tb89QK4pq_AU1UBrOSVtfg5UHPNQwpadwgIE0NlSZhVCxJxvujxPBJ6iwb4o4h6xWHcqAm653K90N3ET-H58YOtatYtRgnK-jvkRZll00-p6jVg8w5_Ckgj0nH98cEZtD2PjPPb8nEeL62VmtqxXF8b-ygVp-s91YLST0DNrjLPXit8aw41Mr9TWN-MWx2qKAvmqVYk4i77UEqlPwgI5Yrg63jTgwvtX7N9O7_XVigAUhfjbIbhqpfQ3Eb5LbrVZzOBJaov15kxQQvNb3gkd4XbsuBpCdzdc2iULz_16WM0MRss7Dj32afDzOoXeBkbaSgI0q8G8hsnIuSLPlF6r_CSvWyPHzRlhUbXFWl--5nmHmzijxqaNx8nzrKBG-2xBdpXYlSn7AxgpkJUrWm97ueBO4U1vZOOahlbUETkGcGBe8-mag8Ar47JfTskFSWgnEUwznytbpARMU4XGb7mWRteLCGANo6Sv-hw52l6RDV7y2r1TOBjiaeQDxY7LLWFOaXXGOfJWNAaSqgzs2q3j6CuSbJLEQLpWqdMIaMD1_CuuZeNPbal9F5Mq_8na5peZy5xqzFsSt_nuXq-C4PrUQkSf87refTy5WGpaaMOhIIMi_7IlfUnDPotUiH6hp1DjLDhZaWfQ8wK8ISH7BhattjVElCByEmghdZIEUDHjIvuR7T2KvCrOpC4cA67sqHbZae1GV6uJq3ou5yf_YzDp7smhDQyOiHg0hKtYQdMstn5DonOnL6x67GxcURW6_BrVM4j0Xg6i_An9Epu92izjtH6kmWWIrn290iggDWWzd0ILNaQ6WXXxEKyMy0YXC1hZmnRCWMHRovcGkELK_fGCxSi2Sabo5X0lPrvYY23_jsEf7hLne1TyyY1BuhuGVgDuw4H7p_LVD7MJlFf5wV7cKh_8IkOmEGMe4i8KaG-C4V6Z6tv-2wdlUPlMU-CE2ifbbw6LmJ0SJxEzBX6qQGcb9I10YDLovuQuszZ4efmMuNHv-wMTURGVbTaFbm58-fD61hFV_RiuSa_4Dsbvz3Gx117k0WxmzQtmfmRYaBPtQJrpckKfRWTbEcKaFZdj-h2y5Nq8DXRac78QMocXtIWD8lAocOrdfALJ9Ban852nHtlN21te9wGCAmqDFSD6Ka_XbetIj-2d6M-9iEb0_XW-luddI5Z0e2ZQt1htaVcau9Dv9zPcF5yTvgSXp24vmw3qBgIBu_eWKotmWuCPgf4xjH29iepxytvj8A_OOZ7OTV-D3_1rLN_nnYn_htx0hvdAQwjx_kKw2fqHg_52ANTJ-5U0mES1gZsremwWB8sObYRpuj2o_Ypt5l7UUU6K2KC_7gGk4hFv8ufvFQBBs0vX9tDgNhbz5yqG-8cnKDJ9L7uoPw5SmSJPZkdapAxnXBAn1QJrOIEXV_C_Ils_6u-5HtcmIVAZzwSMow9--c5-dBE4sVMIcyWwr-PBMCrQZkKuIZ9WLfZVuBDJqFvwsykjqdJqVGS8V4uTSglH9gGbYv60nc-qGe6ymISvBnCs-SqPeReqozwZfGHtUbnOUVcvlrd6A2bdpA-VVQvBGxxDEtbTTVWQNFupSfxIb08aAw4dH1Uba_xtQ5nfo-urk46BRe1KvzuPFy0xQHN_YIXlR1zmSSaDQK8kUPd3ArD9X1bpQsHnPBpMTPjX3FV1r4YbuSg2WbBpVA7rSE6VdGTU0bVjR7Lh0frxsiupu19qqJDAtXBqU0YlI2R0FBtZ8OTowiZedkousQsrnwzRC5SdlQB61g-3Mudj4cIvSfwxQ4gQA7fcAGblzd36D1pB8kdJEWpFcgaZMWFKbMBkPtKlcEYq6_G8226uYMKQpXyPSlbli1X4BmdQzZ__bfAsYRKwTp5V-JCaL0W0AzGGlHN9qDIIkyCG26qQudS9cWCp6XVw2LFhPefXoJpSJbwyiDQn1nQ6lDeT7ElGw1pt_HjcFbtt52S7NBRhC7oqU_TVnp8oWOhvv9VhHow6DbRsMG09gLTooeBqWdIcC07qQvx1FwosSaX2BIAywmoct76vtuPYmz4ePty_ZCjbZf3MqObpCv7f9of8juT53aj_zkfpvtmCuAuiyBr_Hx5rU2XgNAkIXsBYp8Xv1UWHtwe3CETIseT645wr7eOWXWMMnWwtMbRmINEgR8IQUJ8RbkqH-NYTCHy1-3SSYOjtey9hal4UK9fw_gMepb-hu97Ws-5Qp2LHcj7aHMRo578eROpxKFcCcZ1vaWdsYLf2LkLWc7HVjKvvqnzn2Wuj7Lu8i2HFQw_p7mcMXaVfWKvRequfGModJXfvowjo6EQ30XQvkJsnhCjjSSnoO1G2UjJhnp_OxYHsuzWNQLgbbM9oz5LWG7RVbpl5cExyFYPgBvJyW1iHC9aEa6aNMxP_iTwnQBWN2L-BbsxflVNt06ZuFEuRW6I5RtJVjPsb9SRfCqq9yll21lHIqGNxY1JsmOV7bt3RVELs8qiVofgloJ73CuMj_ehxqjUYF8fGyGnpMXutl493ExyDinHAmk263doNkCAbWCUZrZ9lKSb5nV5isnHhAm3FDyrH9r6w16_35DvTsEFrUbNM6cySYUL5WzjtQhQOLiCxx_C4WD-WqyFpuUzu7B76qwyqls4QTGAj9F2_wc0LKGMNI8Kv67LQP82Xx8BdIKEY3NkhkHN77FBSMXn60NdzwZXyaGwLOMy4iXmMKBf5xytakNSQlQjduQ6D2TvbitXJbIopfMKPjsN_kthY-wDZoNVzXyjsx8G9Eu8CuvM_PN0d5z-00V1q_8lvTgX5AHFuTuCYsQLK_bG434Pz5l6vIMzeJMaAVPz8dfWEjrRQU8Yv4LwkuAKQmZ6YeomXO6LE5rFnf9JHryAn5-7Ot9XLk3Z5sMxMHbF2-fJome-IKmy51TVfWT2wVs6vpeYcrvZm6zwqiNwcsopk9a-JBjU2fr2EGdsi5uUGXTEv5llN6QjgOY9e2SxWNqNRGj6vX8j7k2E-RMNl-K_WaZagGKhQX-WHnbKPtse9eN4KF45i64KjuyeTkEjCyFFkbRd8fuv1lSAtM2b_ne_5IL7FZZbYSXpRhbqdvt35GtVgplcWHeWfY0a8Uf3yr9jI_XTZWrAoHj5x5WivjnZvo7QqkI1ujb4_dgcoioLIkwO-jec6y-kDmIadrdHu4HaD89ee1OzE3bGNAdjnFLzk8Ybk4tD5bRiGPwBv_obgAmYyNTdMPaGkb33q8DQvQHu7zTv4_TtUYwgkDv9ytu6kUZlEBtbwPqRISaw2biN5uTjsxX4tWBqRi0zAoiaoN9OCXQ_JJV6H1skIQpst46uiFT7YydkvgiCxR1DMJSoQ6VH-iOfAArtowx4sXgHCAAM_LMXiL6peVEjhJkzTxCpLf6iQ6WbD2vIxkPJOz_8zw9qoPtefttk6GohCgJ_2XCeq1f5OWUQcSiFC8wByxMNARWAtIF4XukwGNtkoGNFCmnX2RhgdhclOmKQjkPJgccRrGbdj4k2DWU-LFB-4EtMMHMJ9A5ey5vs-3yCDvh2eZzHGQDKxZK-VJiuHu4KbdbtaaajinCxFJSA4NTyAGBiCf6TynFnODHd1q_veA6TK3Y53zOMzwl3nh2bN9mlzs4Uy8lqopgchkhtu4OlbapHDmMECn9C-HFesohZwrKLGh2PK9ey9JHA2ejaw-GBkBgVkwaiO2a9WmFYjK7sYo5-Z5K_9x_d8ETc6EcdfD51tdVBl-GHz0H2OKNGADke9wrzLR9UvEJCxOsqelEIjIbqZOTopTn-mxdFrPs1-MbNi-ITKNWeCAq_F2QTyROZ8ph_pjXRKnzKE8atA0hyIT-UchLrd3CX1juupDr5HcsAlxjKXHPA-fUwWqrvtqnzY_52XJ9mHAyaMC5cxM8kq8orXfKMyoSXWJhY0npuMqbDh0Vbid-sZDMrJ45S0cN9Q1sRo1n1Z5BKj64JOMMBghx_zXmusQywsInFwK_N6uYCdZUCnN6hG6yE-hPjP0JASO2h-AowolZBfyG6N09SDXF8gFaOGgwBs8zm4BxFvdU2BSsFSG9b2iXg2IbOcAn576MfMsmz4OqF5p9O6L3fNr72XssudaqTc-D30uN8bCR5ZKiNnrJzsoBwMlOvgm5HZBt7QfsueF2pz5LUO7NEOuR443rcga0SmTHlDGshfHbmNJ81_h8Q9vF8PikXqkTkYRRuGkR0iN86R-D1Af4HP73cLCy54M01FlRSEfdcAOisPNC_A6wZaMa7WKVFCBP8A5m53b0dddl4Ooy04BBOHkMLrFa48_qH7RNtzPmhdC1XCrIfYJAfUJYtd8d9J45GiJVHWW6nmUP4BtsoI4JIbhxdrimC8EztehEfQRoNxDhHJppTeslWGzGWn2cxNh2eQuhspgwxtM2TuO3DkUlqFLYFZFRpe9vrCuh-ECYhKb9jHCgJahsypmOGruqxvdbhz1QuAP8Ke-UM7Y7jQweZ0AZSBIc1p6Z2B4WVV0fJsrDWgQ2i4yOnmUKB34TVTICTSyXfIey8988aigybCEya3HIn08zlwKhbgMtX-IEPLDZAxvK-dVgwhL0APzkceDbTKvwPfZkagOycb_Civ2edM3mpThODfSKvL-vO3ZEJjLdPrarFEU4js4JqCCVPMbDEwufMhjgoruPRu77FP36Q8B6joBjx6_np0x9DIMgYwZPS0Tg630R_jlpz4SBgvs0KGfgoJ-8XwGlOypma5Xubshcbm8Awz6iNTI8DkMl-ZTWiiA8CSd05XoFSMNlkrhyRGntinYYBd4LOYZZKNC35--_qtCE-Gx0bSN6ZJzoT8TrMby0ioEtrn_hIDFtMLcm7oAoN0zdrrSz_RPspX6KMol4Jyk78BrOI1rP7Iz1MSTNWQQkCOa00XRmEJzXcUrHS4_bb9Rj4thma7PzpXopB3HOVuXdy3Givpo_nL0_0vtkuYSacU58GTCJvFJM-k7nAW-44N1cNDutP7k_vK5gqSixxvaUqi87303s-ON6YSn14v0f4wlOhjOxhnM5uMjg-qSrMCq5yZYF-nUwBub6L_Ae86i-CeFH_nvlwxkld1UF3iTprIQOpwlMs \ No newline at end of file diff --git a/main.py b/main.py index 301660b..b096156 100644 --- a/main.py +++ b/main.py @@ -2,7 +2,7 @@ """ Trimble Geodesy Tool - Hauptprogramm mit GUI Geodätische Vermessungsarbeiten mit JXL-Dateien -Überarbeitet basierend auf Benutzer-Feedback +Überarbeitet: Export-Dialog, Georeferenzierung, TreeView """ import sys @@ -19,7 +19,8 @@ from PyQt5.QtWidgets import ( QSpinBox, QDoubleSpinBox, QCheckBox, QSplitter, QFrame, QScrollArea, QHeaderView, QListWidget, QListWidgetItem, QDialog, QDialogButtonBox, QFormLayout, QProgressBar, QStatusBar, QMenuBar, QMenu, QAction, - QToolBar, QStyle, QTreeWidget, QTreeWidgetItem, QAbstractItemView + QToolBar, QStyle, QTreeWidget, QTreeWidgetItem, QAbstractItemView, + QRadioButton, QButtonGroup ) from PyQt5.QtCore import Qt, QThread, pyqtSignal from PyQt5.QtGui import QFont, QIcon, QPalette, QColor, QBrush @@ -32,14 +33,187 @@ from modules.network_adjustment import NetworkAdjustment from modules.reference_point_adjuster import ReferencePointAdjuster, TransformationResult +# ============================================================================= +# Export-Dialog (wiederverwendbar für alle Module) +# ============================================================================= +class ExportFormatDialog(QDialog): + """Dialog zur Auswahl des Export-Formats (COR, CSV, TXT, DXF)""" + + def __init__(self, parent=None, title="Export-Format wählen"): + super().__init__(parent) + self.setWindowTitle(title) + self.setMinimumWidth(300) + self.selected_format = "cor" # Standard + self.setup_ui() + + def setup_ui(self): + layout = QVBoxLayout(self) + + # Format-Auswahl + format_group = QGroupBox("Export-Format auswählen") + format_layout = QVBoxLayout(format_group) + + self.format_group = QButtonGroup(self) + + self.cor_radio = QRadioButton("COR - Koordinatendatei (Standard)") + self.cor_radio.setChecked(True) + self.format_group.addButton(self.cor_radio, 0) + format_layout.addWidget(self.cor_radio) + + self.csv_radio = QRadioButton("CSV - Komma-getrennte Werte") + self.format_group.addButton(self.csv_radio, 1) + format_layout.addWidget(self.csv_radio) + + self.txt_radio = QRadioButton("TXT - Tabulatorgetrennt") + self.format_group.addButton(self.txt_radio, 2) + format_layout.addWidget(self.txt_radio) + + self.dxf_radio = QRadioButton("DXF - AutoCAD Format") + self.format_group.addButton(self.dxf_radio, 3) + format_layout.addWidget(self.dxf_radio) + + layout.addWidget(format_group) + + # Info-Text + info_label = QLabel( + "💡 COR und CSV: PunktID,X,Y,Z (keine Header-Zeile)\n" + " TXT: Tabulatorgetrennt\n" + " DXF: AutoCAD-kompatibel" + ) + info_label.setStyleSheet("color: #666; font-style: italic; padding: 5px;") + layout.addWidget(info_label) + + # Buttons + button_box = QDialogButtonBox( + QDialogButtonBox.Ok | QDialogButtonBox.Cancel + ) + button_box.accepted.connect(self.accept) + button_box.rejected.connect(self.reject) + layout.addWidget(button_box) + + def get_selected_format(self): + """Gibt das ausgewählte Format zurück""" + if self.cor_radio.isChecked(): + return "cor" + elif self.csv_radio.isChecked(): + return "csv" + elif self.txt_radio.isChecked(): + return "txt" + elif self.dxf_radio.isChecked(): + return "dxf" + return "cor" + + def get_file_filter(self): + """Gibt den Dateifilter für den Speicherdialog zurück""" + format_type = self.get_selected_format() + filters = { + 'cor': "COR Files (*.cor)", + 'csv': "CSV Files (*.csv)", + 'txt': "Text Files (*.txt)", + 'dxf': "DXF Files (*.dxf)" + } + return filters.get(format_type, "All Files (*)") + + +def show_export_dialog_and_save(parent, cor_generator, default_name="export"): + """ + Zeigt den Export-Dialog und speichert die Datei. + Wiederverwendbare Funktion für alle Module. + """ + # Format-Dialog anzeigen + dialog = ExportFormatDialog(parent) + if dialog.exec_() != QDialog.Accepted: + return None + + format_type = dialog.get_selected_format() + file_filter = dialog.get_file_filter() + + # Dateiendung hinzufügen + extensions = {'cor': '.cor', 'csv': '.csv', 'txt': '.txt', 'dxf': '.dxf'} + suggested_name = f"{default_name}{extensions.get(format_type, '.cor')}" + + # Datei-Dialog + file_path, _ = QFileDialog.getSaveFileName( + parent, "Speichern unter", suggested_name, file_filter + ) + + if not file_path: + return None + + # Exportieren + try: + if format_type == 'cor': + cor_generator.write_cor_file(file_path) + elif format_type == 'csv': + cor_generator.export_csv(file_path) + elif format_type == 'txt': + cor_generator.export_txt(file_path) + elif format_type == 'dxf': + cor_generator.export_dxf(file_path) + + QMessageBox.information(parent, "Erfolg", f"Datei gespeichert: {file_path}") + return file_path + except Exception as e: + QMessageBox.critical(parent, "Fehler", f"Fehler beim Speichern: {e}") + return None + + +def export_points_with_dialog(parent, points, default_name="punkte"): + """ + Exportiert eine Liste von CORPoint-Objekten mit Export-Dialog. + Universelle Funktion für alle Module. + """ + if not points: + QMessageBox.warning(parent, "Fehler", "Keine Punkte zum Exportieren!") + return None + + # Temporären COR-Generator erstellen + class TempGenerator: + def __init__(self, pts): + self.cor_points = pts + + def write_cor_file(self, path): + lines = [f"{p.name},{p.x:.4f},{p.y:.4f},{p.z:.4f}" for p in self.cor_points] + with open(path, 'w', encoding='utf-8') as f: + f.write("\n".join(lines)) + + def export_csv(self, path): + self.write_cor_file(path) # Gleiches Format + + def export_txt(self, path): + lines = [f"{p.name}\t{p.x:.4f}\t{p.y:.4f}\t{p.z:.4f}" for p in self.cor_points] + with open(path, 'w', encoding='utf-8') as f: + f.write("\n".join(lines)) + + def export_dxf(self, path): + lines = ["0", "SECTION", "2", "ENTITIES"] + for p in self.cor_points: + lines.extend([ + "0", "POINT", "8", "POINTS", + "10", f"{p.x:.4f}", "20", f"{p.y:.4f}", "30", f"{p.z:.4f}", + "0", "TEXT", "8", "NAMES", + "10", f"{p.x + 0.5:.4f}", "20", f"{p.y + 0.5:.4f}", "30", f"{p.z:.4f}", + "40", "0.5", "1", p.name + ]) + lines.extend(["0", "ENDSEC", "0", "EOF"]) + with open(path, 'w', encoding='utf-8') as f: + f.write("\n".join(lines)) + + temp_gen = TempGenerator(points) + return show_export_dialog_and_save(parent, temp_gen, default_name) + + +# ============================================================================= +# JXL-Analyse Tab +# ============================================================================= class JXLAnalysisTab(QWidget): """Tab für JXL-Datei Analyse und Bearbeitung - Mit TreeView für Stationierungen""" def __init__(self, parent=None): super().__init__(parent) self.main_window = parent - self.prism_spin_widgets = {} # {measurement_record_id: QDoubleSpinBox} - self.control_point_checkboxes = {} # {(station_id, point_name): QCheckBox} + self.prism_spin_widgets = {} + self.control_point_checkboxes = {} self.setup_ui() def setup_ui(self): @@ -71,12 +245,12 @@ class JXLAnalysisTab(QWidget): summary_layout = QVBoxLayout(summary_group) self.summary_text = QTextEdit() self.summary_text.setReadOnly(True) - self.summary_text.setMaximumHeight(150) + self.summary_text.setMaximumHeight(120) summary_layout.addWidget(self.summary_text) splitter.addWidget(summary_group) # Stationierungen TreeView - stations_group = QGroupBox("Stationierungen und Messungen") + stations_group = QGroupBox("Stationierungen und Messungen (TreeView)") stations_layout = QVBoxLayout(stations_group) self.stations_tree = QTreeWidget() @@ -85,23 +259,30 @@ class JXLAnalysisTab(QWidget): ]) self.stations_tree.setColumnCount(5) self.stations_tree.setSelectionMode(QAbstractItemView.SingleSelection) - self.stations_tree.header().setSectionResizeMode(0, QHeaderView.Stretch) - self.stations_tree.header().setSectionResizeMode(1, QHeaderView.ResizeToContents) - self.stations_tree.header().setSectionResizeMode(2, QHeaderView.ResizeToContents) - self.stations_tree.header().setSectionResizeMode(3, QHeaderView.ResizeToContents) - self.stations_tree.header().setSectionResizeMode(4, QHeaderView.ResizeToContents) + self.stations_tree.setMinimumHeight(200) + + # Spaltenbreiten + self.stations_tree.setColumnWidth(0, 250) + self.stations_tree.setColumnWidth(1, 120) + self.stations_tree.setColumnWidth(2, 140) + self.stations_tree.setColumnWidth(3, 100) + self.stations_tree.setColumnWidth(4, 60) + stations_layout.addWidget(self.stations_tree) # Info-Label - info_label = QLabel("💡 Tipp: Prismenkonstanten können direkt in der Spalte 'Prismenkonstante' geändert werden.\n" - "Bei freien Stationierungen: Passpunkte mit Checkbox aktivieren/deaktivieren.") + info_label = QLabel( + "💡 Prismenkonstanten können direkt geändert werden. " + "Bei freien Stationierungen: Passpunkte mit Checkbox aktivieren/deaktivieren." + ) info_label.setStyleSheet("color: #666; font-style: italic;") + info_label.setWordWrap(True) stations_layout.addWidget(info_label) splitter.addWidget(stations_group) - # Punkte-Tabelle (kompaktere Ansicht) - points_group = QGroupBox("Alle Punkte") + # Punkte-Tabelle + points_group = QGroupBox("Alle Punkte (Übersicht)") points_layout = QVBoxLayout(points_group) self.points_table = QTableWidget() @@ -123,6 +304,8 @@ class JXLAnalysisTab(QWidget): splitter.addWidget(points_group) + # Splitter-Größen + splitter.setSizes([120, 300, 250]) layout.addWidget(splitter) def browse_file(self): @@ -154,10 +337,10 @@ class JXLAnalysisTab(QWidget): # Zusammenfassung self.summary_text.setText(parser.get_summary()) - # Stationierungen TreeView + # TreeView aktualisieren self.update_stations_tree() - # Punkte-Tabelle aktualisieren + # Punkte-Tabelle self.update_points_table() def update_stations_tree(self): @@ -171,7 +354,6 @@ class JXLAnalysisTab(QWidget): parser = self.main_window.parser - # Stationen durchgehen for station_id, station in parser.stations.items(): # Station als Hauptknoten station_item = QTreeWidgetItem() @@ -179,18 +361,18 @@ class JXLAnalysisTab(QWidget): station_item.setText(1, station.station_type) station_item.setData(0, Qt.UserRole, station_id) - # Hintergrundfarbe für verschiedene Stationstypen + # Hintergrundfarbe if station.station_type == "ReflineStationSetup": - station_item.setBackground(0, QBrush(QColor(200, 230, 200))) # Hellgrün + station_item.setBackground(0, QBrush(QColor(200, 230, 200))) elif station.station_type == "StandardResection": - station_item.setBackground(0, QBrush(QColor(200, 200, 230))) # Hellblau + station_item.setBackground(0, QBrush(QColor(200, 200, 230))) self.stations_tree.addTopLevelItem(station_item) - # Messungen von dieser Station + # Messungen measurements = parser.get_measurements_from_station(station_id) - # Bei freier Stationierung: Qualität der Passpunkte berechnen + # Qualitätsberechnung für freie Stationierung control_point_residuals = {} if station.station_type == "StandardResection": control_point_residuals = self.calculate_control_point_quality( @@ -200,20 +382,20 @@ class JXLAnalysisTab(QWidget): meas_item = QTreeWidgetItem() meas_item.setText(0, f" ↳ {meas.name}") - # Typ ermitteln + # Typ if meas.classification == "BackSight": meas_type = "Passpunkt" else: meas_type = "Messung" meas_item.setText(1, meas_type) - # Prismenkonstante als editierbares SpinBox + # Prismenkonstante SpinBox if meas.target_id and meas.target_id in parser.targets: target = parser.targets[meas.target_id] prism_spin = QDoubleSpinBox() prism_spin.setRange(-100, 100) prism_spin.setDecimals(1) - prism_spin.setValue(target.prism_constant * 1000) # m -> mm + prism_spin.setValue(target.prism_constant * 1000) prism_spin.setSuffix(" mm") prism_spin.setProperty("target_id", meas.target_id) prism_spin.valueChanged.connect( @@ -222,27 +404,26 @@ class JXLAnalysisTab(QWidget): self.prism_spin_widgets[meas.record_id] = prism_spin self.stations_tree.setItemWidget(meas_item, 2, prism_spin) - # Bei freier Stationierung: Passpunkt-Qualität und Checkbox + # Bei freier Stationierung: Qualität und Checkbox if station.station_type == "StandardResection" and meas.classification == "BackSight": - # Qualitätswert anzeigen + # Qualitätswert if meas.name in control_point_residuals: quality_info = control_point_residuals[meas.name] quality_value = quality_info['residual'] rank = quality_info['rank'] total = quality_info['total'] - # Farbcodierung basierend auf Rang quality_label = QLabel(f"{quality_value:.1f} mm") - if rank == 1: # Bester - quality_label.setStyleSheet("background-color: #90EE90; padding: 2px;") # Grün - elif rank == total: # Schlechtester - quality_label.setStyleSheet("background-color: #FFB6C1; padding: 2px;") # Rot - else: # Mittlere - quality_label.setStyleSheet("background-color: #FFFF99; padding: 2px;") # Gelb + if rank == 1: + quality_label.setStyleSheet("background-color: #90EE90; padding: 2px;") + elif rank == total: + quality_label.setStyleSheet("background-color: #FFB6C1; padding: 2px;") + else: + quality_label.setStyleSheet("background-color: #FFFF99; padding: 2px;") self.stations_tree.setItemWidget(meas_item, 3, quality_label) - # Checkbox für Aktivierung/Deaktivierung + # Checkbox checkbox = QCheckBox() checkbox.setChecked(not meas.deleted) checkbox.setProperty("station_id", station_id) @@ -259,47 +440,33 @@ class JXLAnalysisTab(QWidget): station_item.setExpanded(True) def calculate_control_point_quality(self, station_id, station, measurements): - """ - Berechnet die Qualität der Passpunkte einer freien Stationierung. - Nutzt die Residuen aus der Stationierungsberechnung. - """ + """Berechnet Qualität der Passpunkte""" parser = self.main_window.parser control_points = [] for meas in measurements: if meas.classification == "BackSight" and not meas.deleted: - # Residuum berechnen: Differenz zwischen gemessenen und berechneten Koordinaten - # Vereinfachte Berechnung basierend auf Streckendifferenz if meas.edm_distance is not None and meas.name in parser.points: target_point = parser.points[meas.name] if target_point.east is not None and target_point.north is not None: if station.east is not None and station.north is not None: - # Berechnete Strecke dx = target_point.east - station.east dy = target_point.north - station.north calc_dist = (dx**2 + dy**2)**0.5 - - # Residuum (Differenz zur gemessenen Strecke) - residual = abs(meas.edm_distance - calc_dist) * 1000 # in mm + residual = abs(meas.edm_distance - calc_dist) * 1000 control_points.append((meas.name, residual)) - # Sortieren und Ränge vergeben if not control_points: return {} control_points.sort(key=lambda x: x[1]) result = {} for rank, (name, residual) in enumerate(control_points, 1): - result[name] = { - 'residual': residual, - 'rank': rank, - 'total': len(control_points) - } + result[name] = {'residual': residual, 'rank': rank, 'total': len(control_points)} return result def on_prism_changed(self, target_id, new_value_mm): - """Wird aufgerufen, wenn eine Prismenkonstante geändert wird""" if self.main_window.parser and target_id in self.main_window.parser.targets: new_value_m = new_value_mm / 1000.0 self.main_window.parser.modify_prism_constant(target_id, new_value_m) @@ -307,14 +474,11 @@ class JXLAnalysisTab(QWidget): f"Prismenkonstante für {target_id} auf {new_value_mm:.1f} mm gesetzt") def on_control_point_toggled(self, station_id, point_name, state): - """Wird aufgerufen, wenn ein Passpunkt aktiviert/deaktiviert wird""" is_active = state == Qt.Checked - # Hier könnte man die Stationierung neu berechnen self.main_window.statusBar().showMessage( f"Passpunkt {point_name} {'aktiviert' if is_active else 'deaktiviert'}") def update_points_table(self): - """Aktualisiert die Punkte-Tabelle""" if not self.main_window.parser: return @@ -343,6 +507,9 @@ class JXLAnalysisTab(QWidget): self.update_display() +# ============================================================================= +# COR-Generator Tab +# ============================================================================= class CORGeneratorTab(QWidget): """Tab für COR-Datei Generierung""" @@ -367,15 +534,12 @@ class CORGeneratorTab(QWidget): ]) options_layout.addWidget(self.method_combo, 0, 1) - self.include_header_check = QCheckBox("Stations-Header einfügen") - self.include_header_check.setChecked(True) - options_layout.addWidget(self.include_header_check, 1, 0, 1, 2) - layout.addWidget(options_group) # Generieren Button - generate_btn = QPushButton("COR-Datei generieren") + generate_btn = QPushButton("Punkte generieren") generate_btn.clicked.connect(self.generate_cor) + generate_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;") layout.addWidget(generate_btn) # Vorschau @@ -396,27 +560,11 @@ class CORGeneratorTab(QWidget): layout.addWidget(preview_group) - # Export - export_group = QGroupBox("Export") - export_layout = QHBoxLayout(export_group) - - export_cor_btn = QPushButton("Als COR speichern") - export_cor_btn.clicked.connect(lambda: self.export_file('cor')) - export_layout.addWidget(export_cor_btn) - - export_csv_btn = QPushButton("Als CSV speichern") - export_csv_btn.clicked.connect(lambda: self.export_file('csv')) - export_layout.addWidget(export_csv_btn) - - export_txt_btn = QPushButton("Als TXT speichern") - export_txt_btn.clicked.connect(lambda: self.export_file('txt')) - export_layout.addWidget(export_txt_btn) - - export_dxf_btn = QPushButton("Als DXF speichern") - export_dxf_btn.clicked.connect(lambda: self.export_file('dxf')) - export_layout.addWidget(export_dxf_btn) - - layout.addWidget(export_group) + # Export Button (mit Dialog) + export_btn = QPushButton("📥 Punkte exportieren...") + export_btn.clicked.connect(self.export_with_dialog) + export_btn.setStyleSheet("background-color: #2196F3; color: white; font-weight: bold; font-size: 14px; padding: 10px;") + layout.addWidget(export_btn) def generate_cor(self): if not self.main_window.parser: @@ -438,41 +586,22 @@ class CORGeneratorTab(QWidget): self.preview_table.setItem(row, 2, QTableWidgetItem(f"{p.y:.4f}")) self.preview_table.setItem(row, 3, QTableWidgetItem(f"{p.z:.4f}")) - # Statistiken self.stats_text.setText(self.cor_generator.get_statistics()) - self.main_window.statusBar().showMessage(f"{len(points)} Punkte generiert") - def export_file(self, format_type): + def export_with_dialog(self): if not self.cor_generator or not self.cor_generator.cor_points: QMessageBox.warning(self, "Fehler", "Bitte zuerst Punkte generieren!") return - filters = { - 'cor': "COR Files (*.cor)", - 'csv': "CSV Files (*.csv)", - 'txt': "Text Files (*.txt)", - 'dxf': "DXF Files (*.dxf)" - } - - file_path, _ = QFileDialog.getSaveFileName( - self, "Speichern unter", "", filters.get(format_type, "All Files (*)")) - - if file_path: - if format_type == 'cor': - self.cor_generator.write_cor_file(file_path, self.include_header_check.isChecked()) - elif format_type == 'csv': - self.cor_generator.export_csv(file_path) - elif format_type == 'txt': - self.cor_generator.export_txt(file_path) - elif format_type == 'dxf': - self.cor_generator.export_dxf(file_path) - - QMessageBox.information(self, "Erfolg", f"Datei gespeichert: {file_path}") + show_export_dialog_and_save(self, self.cor_generator, "koordinaten") +# ============================================================================= +# Transformation Tab (Y-Richtung entfernt) +# ============================================================================= class TransformationTab(QWidget): - """Tab für Koordinatentransformation - Überarbeitet""" + """Tab für Koordinatentransformation - Y-Richtung entfernt""" def __init__(self, parent=None): super().__init__(parent) @@ -544,7 +673,7 @@ class TransformationTab(QWidget): layout.addWidget(self.manual_group) - # 2-Punkte-Definition (überarbeitet) + # 2-Punkte-Definition (OHNE Y-Richtung!) self.twopoint_group = QGroupBox("2-Punkte-Definition") twopoint_layout = QGridLayout(self.twopoint_group) @@ -552,28 +681,24 @@ class TransformationTab(QWidget): self.xy_origin_combo = QComboBox() twopoint_layout.addWidget(self.xy_origin_combo, 0, 1) - twopoint_layout.addWidget(QLabel("Y-Richtung:"), 1, 0) - self.direction_combo = QComboBox() - twopoint_layout.addWidget(self.direction_combo, 1, 1) - - twopoint_layout.addWidget(QLabel("Z-Nullpunkt (0):"), 2, 0) + twopoint_layout.addWidget(QLabel("Z-Nullpunkt (0):"), 1, 0) self.z_origin_combo = QComboBox() - twopoint_layout.addWidget(self.z_origin_combo, 2, 1) + twopoint_layout.addWidget(self.z_origin_combo, 1, 1) refresh_btn = QPushButton("Punktliste aktualisieren") refresh_btn.clicked.connect(self.refresh_point_lists) - twopoint_layout.addWidget(refresh_btn, 3, 0, 1, 2) + twopoint_layout.addWidget(refresh_btn, 2, 0, 1, 2) self.twopoint_group.setVisible(False) layout.addWidget(self.twopoint_group) - # Transformation durchführen + # Transformation berechnen transform_btn = QPushButton("Transformation berechnen") transform_btn.clicked.connect(self.execute_transformation) transform_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;") layout.addWidget(transform_btn) - # Anwenden Button (NEU - Bug Fix) + # Anwenden Button apply_btn = QPushButton("Transformation anwenden (Punktliste aktualisieren)") apply_btn.clicked.connect(self.apply_transformation) apply_btn.setStyleSheet("background-color: #2196F3; color: white; font-weight: bold;") @@ -593,7 +718,7 @@ class TransformationTab(QWidget): export_report_btn.clicked.connect(self.export_report) export_layout.addWidget(export_report_btn) - export_points_btn = QPushButton("Punkte exportieren") + export_points_btn = QPushButton("📥 Punkte exportieren...") export_points_btn.clicked.connect(self.export_points) export_layout.addWidget(export_points_btn) @@ -615,17 +740,13 @@ class TransformationTab(QWidget): points = list(self.main_window.parser.get_active_points().keys()) self.xy_origin_combo.clear() - self.direction_combo.clear() self.z_origin_combo.clear() - # Standard-Vorschläge: 7001 für XY, 7002 für Z default_xy = None - default_direction = None default_z = None for name in sorted(points): self.xy_origin_combo.addItem(name) - self.direction_combo.addItem(name) self.z_origin_combo.addItem(name) if name == "7001": @@ -633,7 +754,6 @@ class TransformationTab(QWidget): if name == "7002": default_z = name - # Standard-Werte setzen falls vorhanden if default_xy: idx = self.xy_origin_combo.findText(default_xy) if idx >= 0: @@ -643,18 +763,12 @@ class TransformationTab(QWidget): idx = self.z_origin_combo.findText(default_z) if idx >= 0: self.z_origin_combo.setCurrentIndex(idx) - - # Info anzeigen - if default_xy or default_z: - self.main_window.statusBar().showMessage( - f"Standard-Vorschlag: XY={default_xy or 'nicht gefunden'}, Z={default_z or 'nicht gefunden'}") def execute_transformation(self): if not self.main_window.parser: QMessageBox.warning(self, "Fehler", "Bitte zuerst eine JXL-Datei laden!") return - # Punkte aus Parser holen points = [] for name, p in self.main_window.parser.get_active_points().items(): if p.east is not None and p.north is not None: @@ -678,25 +792,23 @@ class TransformationTab(QWidget): ) elif self.twopoint_radio.isChecked(): origin = self.xy_origin_combo.currentText() - direction = self.direction_combo.currentText() zref = self.z_origin_combo.currentText() - if not self.transformer.compute_from_two_points(origin, direction, zref): - QMessageBox.warning(self, "Fehler", "Punkte nicht gefunden!") + # Transformation zum Nullpunkt (nur Translation, keine Rotation) + if not self.transformer.compute_translation_only(origin, zref): + QMessageBox.warning(self, "Fehler", "Punkt nicht gefunden!") return self.transformer.transform() - # Ergebnisse anzeigen report = self.transformer.get_parameters_report() report += "\n\n" report += self.transformer.get_comparison_table() self.results_text.setText(report) - self.main_window.statusBar().showMessage("Transformation berechnet (noch nicht angewendet)") + self.main_window.statusBar().showMessage("Transformation berechnet") def apply_transformation(self): - """Wendet die Transformation auf die Punktliste an (Bug Fix)""" if not self.transformer.transformed_points: QMessageBox.warning(self, "Fehler", "Bitte zuerst 'Transformation berechnen' ausführen!") @@ -707,14 +819,12 @@ class TransformationTab(QWidget): reply = QMessageBox.question( self, "Bestätigung", - "Sollen die transformierten Koordinaten auf alle Punkte angewendet werden?\n\n" - "Dies ändert die Koordinaten im Speicher.", + "Transformierte Koordinaten auf alle Punkte anwenden?", QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.No: return - # Transformierte Koordinaten in Parser übernehmen for trans_point in self.transformer.transformed_points: if trans_point.name in self.main_window.parser.points: p = self.main_window.parser.points[trans_point.name] @@ -722,18 +832,14 @@ class TransformationTab(QWidget): p.north = trans_point.y p.elevation = trans_point.z - # GUI aktualisieren - # JXL-Tab aktualisieren jxl_tab = self.main_window.tabs.widget(0) if hasattr(jxl_tab, 'update_display'): jxl_tab.update_display() - # Erfolgsmeldung QMessageBox.information(self, "Erfolg", - f"{len(self.transformer.transformed_points)} Punkte wurden transformiert!\n\n" - "Die Punktliste wurde aktualisiert.") + f"{len(self.transformer.transformed_points)} Punkte transformiert!") - self.main_window.statusBar().showMessage("Transformation angewendet - Punktliste aktualisiert") + self.main_window.statusBar().showMessage("Transformation angewendet") def export_report(self): file_path, _ = QFileDialog.getSaveFileName( @@ -745,73 +851,83 @@ class TransformationTab(QWidget): def export_points(self): if not self.transformer.transformed_points: - QMessageBox.warning(self, "Fehler", "Keine transformierten Punkte vorhanden!") + QMessageBox.warning(self, "Fehler", "Keine transformierten Punkte!") return - file_path, _ = QFileDialog.getSaveFileName( - self, "Punkte speichern", "", "CSV Files (*.csv)") - if file_path: - lines = ["Punkt;X;Y;Z"] - for p in self.transformer.transformed_points: - lines.append(f"{p.name};{p.x:.4f};{p.y:.4f};{p.z:.4f}") - - with open(file_path, 'w', encoding='utf-8') as f: - f.write("\n".join(lines)) - QMessageBox.information(self, "Erfolg", f"Punkte gespeichert: {file_path}") + export_points_with_dialog(self, self.transformer.transformed_points, "transformiert") +# ============================================================================= +# Georeferenzierung Tab (KOMPLETT NEU) +# ============================================================================= class GeoreferencingTab(QWidget): - """Tab für Georeferenzierung""" + """Tab für Georeferenzierung - NEUER WORKFLOW mit Punktdatei-Laden""" def __init__(self, parent=None): super().__init__(parent) self.main_window = parent self.georeferencer = Georeferencer() + self.loaded_target_points = {} # {name: (x, y, z)} + self.point_assignments = {} # {row: (target_name, jxl_name)} self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) - # Passpunkte - cp_group = QGroupBox("Passpunkte (mind. 3)") - cp_layout = QVBoxLayout(cp_group) + # Schritt 1: Punktdatei laden + load_group = QGroupBox("Schritt 1: Soll-Koordinaten laden") + load_layout = QVBoxLayout(load_group) - self.cp_table = QTableWidget() - self.cp_table.setColumnCount(7) - self.cp_table.setHorizontalHeaderLabels([ - "Punkt", "X_lokal", "Y_lokal", "Z_lokal", "X_Ziel", "Y_Ziel", "Z_Ziel"]) - self.cp_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - cp_layout.addWidget(self.cp_table) + load_btn_layout = QHBoxLayout() + self.load_file_btn = QPushButton("📂 Punktdatei laden (COR/CSV)") + self.load_file_btn.clicked.connect(self.load_target_file) + self.load_file_btn.setStyleSheet("background-color: #FF9800; color: white; font-weight: bold;") + load_btn_layout.addWidget(self.load_file_btn) - # Passpunkt-Buttons - cp_buttons = QHBoxLayout() + self.loaded_file_label = QLabel("Keine Datei geladen") + self.loaded_file_label.setStyleSheet("color: #666;") + load_btn_layout.addWidget(self.loaded_file_label) + load_btn_layout.addStretch() - add_cp_btn = QPushButton("Passpunkt hinzufügen") - add_cp_btn.clicked.connect(self.add_control_point) - cp_buttons.addWidget(add_cp_btn) + load_layout.addLayout(load_btn_layout) + layout.addWidget(load_group) - remove_cp_btn = QPushButton("Entfernen") - remove_cp_btn.clicked.connect(self.remove_control_point) - cp_buttons.addWidget(remove_cp_btn) + # Schritt 2: Punkt-Zuordnungstabelle + assign_group = QGroupBox("Schritt 2: Punkt-Zuordnung (Soll → Ist aus JXL)") + assign_layout = QVBoxLayout(assign_group) - load_local_btn = QPushButton("Lokale Koordinaten aus JXL") - load_local_btn.clicked.connect(self.load_local_from_jxl) - cp_buttons.addWidget(load_local_btn) + info_label = QLabel( + "💡 Wählen Sie für jeden geladenen Punkt den entsprechenden Punkt aus der JXL-Datei." + ) + info_label.setStyleSheet("color: #666; font-style: italic;") + assign_layout.addWidget(info_label) - load_target_btn = QPushButton("Zielkoordinaten importieren") - load_target_btn.clicked.connect(self.load_target_coords) - cp_buttons.addWidget(load_target_btn) + self.assign_table = QTableWidget() + self.assign_table.setColumnCount(8) + self.assign_table.setHorizontalHeaderLabels([ + "Soll-Punkt", "X_Soll", "Y_Soll", "Z_Soll", + "JXL-Punkt ⬇", "X_Ist", "Y_Ist", "Z_Ist" + ]) + self.assign_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + assign_layout.addWidget(self.assign_table) - cp_buttons.addStretch() - cp_layout.addLayout(cp_buttons) + layout.addWidget(assign_group) - layout.addWidget(cp_group) + # Schritt 3: Berechnung + calc_group = QGroupBox("Schritt 3: Georeferenzierung durchführen") + calc_layout = QHBoxLayout(calc_group) - # Transformation berechnen - calc_btn = QPushButton("Transformation berechnen") + calc_btn = QPushButton("🔄 Transformation berechnen") calc_btn.clicked.connect(self.calculate_transformation) - calc_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;") - layout.addWidget(calc_btn) + calc_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold; font-size: 14px; padding: 10px;") + calc_layout.addWidget(calc_btn) + + apply_btn = QPushButton("✓ Auf alle Punkte anwenden") + apply_btn.clicked.connect(self.apply_to_all_points) + apply_btn.setStyleSheet("background-color: #2196F3; color: white; font-weight: bold;") + calc_layout.addWidget(apply_btn) + + layout.addWidget(calc_group) # Ergebnisse results_group = QGroupBox("Ergebnisse") @@ -819,7 +935,7 @@ class GeoreferencingTab(QWidget): self.results_text = QTextEdit() self.results_text.setReadOnly(True) - self.results_text.setFont(QFont("Courier")) + self.results_text.setFont(QFont("Courier", 9)) results_layout.addWidget(self.results_text) # Export @@ -829,127 +945,220 @@ class GeoreferencingTab(QWidget): export_report_btn.clicked.connect(self.export_report) export_layout.addWidget(export_report_btn) - transform_all_btn = QPushButton("Alle Punkte transformieren") - transform_all_btn.clicked.connect(self.transform_all_points) - export_layout.addWidget(transform_all_btn) + export_points_btn = QPushButton("📥 Punkte exportieren...") + export_points_btn.clicked.connect(self.export_transformed_points) + export_layout.addWidget(export_points_btn) results_layout.addLayout(export_layout) layout.addWidget(results_group) - def add_control_point(self): - row = self.cp_table.rowCount() - self.cp_table.insertRow(row) - - # Punkt-Auswahl - combo = QComboBox() - if self.main_window.parser: - for name in sorted(self.main_window.parser.get_active_points().keys()): - combo.addItem(name) - self.cp_table.setCellWidget(row, 0, combo) - - # Editierbare Felder für Koordinaten - for col in range(1, 7): - spin = QDoubleSpinBox() - spin.setRange(-10000000, 10000000) - spin.setDecimals(4) - self.cp_table.setCellWidget(row, col, spin) - - def remove_control_point(self): - row = self.cp_table.currentRow() - if row >= 0: - self.cp_table.removeRow(row) - - def load_local_from_jxl(self): - if not self.main_window.parser: - QMessageBox.warning(self, "Fehler", "Bitte zuerst eine JXL-Datei laden!") - return - - for row in range(self.cp_table.rowCount()): - combo = self.cp_table.cellWidget(row, 0) - if combo: - name = combo.currentText() - if name in self.main_window.parser.points: - p = self.main_window.parser.points[name] - - self.cp_table.cellWidget(row, 1).setValue(p.east or 0) - self.cp_table.cellWidget(row, 2).setValue(p.north or 0) - self.cp_table.cellWidget(row, 3).setValue(p.elevation or 0) - - def load_target_coords(self): + def load_target_file(self): + """Lädt eine COR/CSV-Datei mit Soll-Koordinaten""" file_path, _ = QFileDialog.getOpenFileName( - self, "Zielkoordinaten laden", "", - "CSV Files (*.csv);;Text Files (*.txt);;All Files (*)") + self, "Soll-Koordinaten laden", "", + "Koordinatendateien (*.cor *.csv *.txt);;All Files (*)") if not file_path: return try: + self.loaded_target_points.clear() + with open(file_path, 'r', encoding='utf-8') as f: lines = f.readlines() - # Format: Punkt;X;Y;Z oder Punkt,X,Y,Z - coord_dict = {} for line in lines: line = line.strip() if not line or line.startswith('#'): continue - parts = line.replace(',', ';').split(';') + # Komma oder Semikolon als Trenner + parts = line.replace(';', ',').split(',') if len(parts) >= 4: name = parts[0].strip() - x = float(parts[1]) - y = float(parts[2]) - z = float(parts[3]) - coord_dict[name] = (x, y, z) + try: + x = float(parts[1].strip()) + y = float(parts[2].strip()) + z = float(parts[3].strip()) + self.loaded_target_points[name] = (x, y, z) + except ValueError: + continue # Header oder ungültige Zeile überspringen - # In Tabelle eintragen - for row in range(self.cp_table.rowCount()): - combo = self.cp_table.cellWidget(row, 0) - if combo: - name = combo.currentText() - if name in coord_dict: - x, y, z = coord_dict[name] - self.cp_table.cellWidget(row, 4).setValue(x) - self.cp_table.cellWidget(row, 5).setValue(y) - self.cp_table.cellWidget(row, 6).setValue(z) + self.loaded_file_label.setText(f"✓ {os.path.basename(file_path)} ({len(self.loaded_target_points)} Punkte)") + self.loaded_file_label.setStyleSheet("color: green; font-weight: bold;") - QMessageBox.information(self, "Erfolg", f"{len(coord_dict)} Koordinaten geladen!") + # Tabelle aktualisieren + self.update_assignment_table() + + QMessageBox.information(self, "Erfolg", + f"{len(self.loaded_target_points)} Punkte geladen!") except Exception as e: QMessageBox.critical(self, "Fehler", f"Fehler beim Laden: {e}") + def update_assignment_table(self): + """Aktualisiert die Zuordnungstabelle""" + self.assign_table.setRowCount(len(self.loaded_target_points)) + + # JXL-Punkte für Dropdown + jxl_points = ["-- Nicht zugeordnet --"] + if self.main_window.parser: + jxl_points.extend(sorted(self.main_window.parser.get_active_points().keys())) + + for row, (name, (x, y, z)) in enumerate(sorted(self.loaded_target_points.items())): + # Soll-Punkt Name + self.assign_table.setItem(row, 0, QTableWidgetItem(name)) + + # Soll-Koordinaten + self.assign_table.setItem(row, 1, QTableWidgetItem(f"{x:.4f}")) + self.assign_table.setItem(row, 2, QTableWidgetItem(f"{y:.4f}")) + self.assign_table.setItem(row, 3, QTableWidgetItem(f"{z:.4f}")) + + # JXL-Punkt Dropdown + combo = QComboBox() + combo.addItems(jxl_points) + + # Automatische Zuordnung: gleicher Name? + if name in jxl_points: + idx = combo.findText(name) + if idx >= 0: + combo.setCurrentIndex(idx) + + combo.currentTextChanged.connect( + lambda text, r=row: self.on_jxl_point_selected(r, text)) + self.assign_table.setCellWidget(row, 4, combo) + + # Ist-Koordinaten (werden bei Auswahl gefüllt) + self.assign_table.setItem(row, 5, QTableWidgetItem("")) + self.assign_table.setItem(row, 6, QTableWidgetItem("")) + self.assign_table.setItem(row, 7, QTableWidgetItem("")) + + # Initiale Zuordnung auslösen + self.on_jxl_point_selected(row, combo.currentText()) + + def on_jxl_point_selected(self, row, jxl_name): + """Wird aufgerufen, wenn ein JXL-Punkt ausgewählt wird""" + if jxl_name == "-- Nicht zugeordnet --" or not self.main_window.parser: + self.assign_table.setItem(row, 5, QTableWidgetItem("")) + self.assign_table.setItem(row, 6, QTableWidgetItem("")) + self.assign_table.setItem(row, 7, QTableWidgetItem("")) + return + + if jxl_name in self.main_window.parser.points: + p = self.main_window.parser.points[jxl_name] + self.assign_table.setItem(row, 5, QTableWidgetItem(f"{p.east:.4f}" if p.east else "")) + self.assign_table.setItem(row, 6, QTableWidgetItem(f"{p.north:.4f}" if p.north else "")) + self.assign_table.setItem(row, 7, QTableWidgetItem(f"{p.elevation:.4f}" if p.elevation else "")) + def calculate_transformation(self): + """Berechnet die Transformation basierend auf den Punkt-Paaren""" + if not self.loaded_target_points: + QMessageBox.warning(self, "Fehler", "Bitte zuerst eine Punktdatei laden!") + return + + if not self.main_window.parser: + QMessageBox.warning(self, "Fehler", "Bitte zuerst eine JXL-Datei laden!") + return + self.georeferencer.clear_control_points() - for row in range(self.cp_table.rowCount()): - combo = self.cp_table.cellWidget(row, 0) + # Punkt-Paare sammeln + valid_pairs = 0 + for row in range(self.assign_table.rowCount()): + combo = self.assign_table.cellWidget(row, 4) if not combo: continue - name = combo.currentText() - local_x = self.cp_table.cellWidget(row, 1).value() - local_y = self.cp_table.cellWidget(row, 2).value() - local_z = self.cp_table.cellWidget(row, 3).value() - target_x = self.cp_table.cellWidget(row, 4).value() - target_y = self.cp_table.cellWidget(row, 5).value() - target_z = self.cp_table.cellWidget(row, 6).value() + jxl_name = combo.currentText() + if jxl_name == "-- Nicht zugeordnet --": + continue + + # Soll-Koordinaten + target_name = self.assign_table.item(row, 0).text() + if target_name not in self.loaded_target_points: + continue + + target_x, target_y, target_z = self.loaded_target_points[target_name] + + # Ist-Koordinaten aus JXL + if jxl_name not in self.main_window.parser.points: + continue + + p = self.main_window.parser.points[jxl_name] + if p.east is None or p.north is None: + continue + + local_x = p.east + local_y = p.north + local_z = p.elevation or 0 self.georeferencer.add_control_point( - name, local_x, local_y, local_z, target_x, target_y, target_z) + jxl_name, local_x, local_y, local_z, target_x, target_y, target_z + ) + valid_pairs += 1 - if len(self.georeferencer.control_points) < 3: - QMessageBox.warning(self, "Fehler", "Mindestens 3 Passpunkte erforderlich!") + if valid_pairs < 2: + QMessageBox.warning(self, "Fehler", + f"Mindestens 2 gültige Punkt-Paare erforderlich!\n" + f"Aktuell: {valid_pairs}") return try: self.georeferencer.compute_transformation() report = self.georeferencer.get_transformation_report() self.results_text.setText(report) - self.main_window.statusBar().showMessage("Georeferenzierung berechnet") + self.main_window.statusBar().showMessage( + f"Georeferenzierung berechnet ({valid_pairs} Passpunkte)") except Exception as e: QMessageBox.critical(self, "Fehler", f"Berechnung fehlgeschlagen: {e}") + def apply_to_all_points(self): + """Wendet die Transformation auf alle JXL-Punkte an""" + if self.georeferencer.result is None: + QMessageBox.warning(self, "Fehler", "Bitte zuerst Transformation berechnen!") + return + + if not self.main_window.parser: + return + + reply = QMessageBox.question( + self, "Bestätigung", + "Transformation auf alle Punkte anwenden?", + QMessageBox.Yes | QMessageBox.No) + + if reply == QMessageBox.No: + return + + # Punkte transformieren + points = [] + for name, p in self.main_window.parser.get_active_points().items(): + if p.east is not None and p.north is not None: + points.append(CORPoint( + name=name, x=p.east, y=p.north, z=p.elevation or 0 + )) + + self.georeferencer.set_points_to_transform(points) + transformed = self.georeferencer.transform_points() + + # In Parser übernehmen + for tp in transformed: + if tp.name in self.main_window.parser.points: + p = self.main_window.parser.points[tp.name] + p.east = tp.x + p.north = tp.y + p.elevation = tp.z + + # GUI aktualisieren + jxl_tab = self.main_window.tabs.widget(0) + if hasattr(jxl_tab, 'update_display'): + jxl_tab.update_display() + + QMessageBox.information(self, "Erfolg", + f"{len(transformed)} Punkte georeferenziert!") + + self.main_window.statusBar().showMessage("Georeferenzierung angewendet") + def export_report(self): file_path, _ = QFileDialog.getSaveFileName( self, "Bericht speichern", "", "Text Files (*.txt)") @@ -958,7 +1167,7 @@ class GeoreferencingTab(QWidget): f.write(self.results_text.toPlainText()) QMessageBox.information(self, "Erfolg", f"Bericht gespeichert: {file_path}") - def transform_all_points(self): + def export_transformed_points(self): if self.georeferencer.result is None: QMessageBox.warning(self, "Fehler", "Bitte zuerst Transformation berechnen!") return @@ -966,34 +1175,25 @@ class GeoreferencingTab(QWidget): if not self.main_window.parser: return - # Punkte transformieren + # Punkte transformieren für Export points = [] for name, p in self.main_window.parser.get_active_points().items(): if p.east is not None and p.north is not None: points.append(CORPoint( - name=name, x=p.east, y=p.north, z=p.elevation or 0)) + name=name, x=p.east, y=p.north, z=p.elevation or 0 + )) self.georeferencer.set_points_to_transform(points) transformed = self.georeferencer.transform_points() - # Export-Dialog - file_path, _ = QFileDialog.getSaveFileName( - self, "Transformierte Punkte speichern", "", "CSV Files (*.csv)") - - if file_path: - lines = ["Punkt;X;Y;Z"] - for p in transformed: - lines.append(f"{p.name};{p.x:.4f};{p.y:.4f};{p.z:.4f}") - - with open(file_path, 'w', encoding='utf-8') as f: - f.write("\n".join(lines)) - - QMessageBox.information(self, "Erfolg", - f"{len(transformed)} Punkte transformiert und gespeichert!") + export_points_with_dialog(self, transformed, "georeferenziert") +# ============================================================================= +# Netzausgleichung Tab +# ============================================================================= class NetworkAdjustmentTab(QWidget): - """Tab für Netzausgleichung - Überarbeitet mit automatischer Punkterkennung""" + """Tab für Netzausgleichung""" def __init__(self, parent=None): super().__init__(parent) @@ -1036,23 +1236,21 @@ class NetworkAdjustmentTab(QWidget): points_group = QGroupBox("Automatisch erkannte Punkttypen") points_layout = QVBoxLayout(points_group) - # Info-Label info_label = QLabel( - "💡 Das Programm erkennt automatisch:\n" - " • Festpunkte: Alle Punkte, die in Stationierungen verwendet werden (1000er, 2000er Serie)\n" - " • Messpunkte: 3000er Punkte (Detailmessungen)") + "💡 Automatische Erkennung:\n" + " • Festpunkte: Stationspunkte und Anschlusspunkte\n" + " • Messpunkte: 3000er Punkte" + ) info_label.setStyleSheet("color: #666; background-color: #f0f0f0; padding: 10px;") points_layout.addWidget(info_label) - # Tabelle für erkannte Punkte self.points_table = QTableWidget() self.points_table.setColumnCount(4) self.points_table.setHorizontalHeaderLabels(["Punkt", "Typ", "X", "Y"]) self.points_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - self.points_table.setMaximumHeight(200) + self.points_table.setMaximumHeight(150) points_layout.addWidget(self.points_table) - # Button zum Aktualisieren refresh_btn = QPushButton("Punkte automatisch erkennen") refresh_btn.clicked.connect(self.auto_detect_points) refresh_btn.setStyleSheet("background-color: #FF9800; color: white;") @@ -1082,7 +1280,7 @@ class NetworkAdjustmentTab(QWidget): export_report_btn.clicked.connect(self.export_report) export_layout.addWidget(export_report_btn) - export_points_btn = QPushButton("Koordinaten exportieren") + export_points_btn = QPushButton("📥 Koordinaten exportieren...") export_points_btn.clicked.connect(self.export_points) export_layout.addWidget(export_points_btn) @@ -1090,7 +1288,6 @@ class NetworkAdjustmentTab(QWidget): layout.addWidget(results_group) def auto_detect_points(self): - """Erkennt automatisch Festpunkte und Messpunkte""" if not self.main_window.parser: QMessageBox.warning(self, "Fehler", "Bitte zuerst eine JXL-Datei laden!") return @@ -1099,37 +1296,29 @@ class NetworkAdjustmentTab(QWidget): self.fixed_points.clear() self.measurement_points.clear() - # Festpunkte: Punkte die in Stationierungen verwendet werden - # Das sind typischerweise 1000er (Stationen) und 2000er (Anschlusspunkte) for station_id, station in parser.stations.items(): - # Station selbst ist Festpunkt if station.name: self.fixed_points.add(station.name) - # Anschlusspunkte aus Backbearings for bb_id, bb in parser.backbearings.items(): if bb.station_record_id == station_id and bb.backsight: self.fixed_points.add(bb.backsight) - # Anschlusspunkte aus Messungen (BackSight Classification) measurements = parser.get_measurements_from_station(station_id) for meas in measurements: if meas.classification == "BackSight" and meas.name: self.fixed_points.add(meas.name) - # Messpunkte: 3000er Serie for name in parser.get_active_points().keys(): if name.startswith("3"): self.measurement_points.add(name) - # Tabelle aktualisieren self.update_points_table() self.main_window.statusBar().showMessage( f"Erkannt: {len(self.fixed_points)} Festpunkte, {len(self.measurement_points)} Messpunkte") def update_points_table(self): - """Aktualisiert die Tabelle mit erkannten Punkten""" parser = self.main_window.parser if not parser: return @@ -1138,19 +1327,16 @@ class NetworkAdjustmentTab(QWidget): self.points_table.setRowCount(len(all_points)) for row, name in enumerate(sorted(all_points)): - # Punkt-Name self.points_table.setItem(row, 0, QTableWidgetItem(name)) - # Typ if name in self.fixed_points: type_item = QTableWidgetItem("Festpunkt") - type_item.setBackground(QBrush(QColor(200, 230, 200))) # Hellgrün + type_item.setBackground(QBrush(QColor(200, 230, 200))) else: type_item = QTableWidgetItem("Messpunkt") - type_item.setBackground(QBrush(QColor(200, 200, 230))) # Hellblau + type_item.setBackground(QBrush(QColor(200, 200, 230))) self.points_table.setItem(row, 1, type_item) - # Koordinaten if name in parser.points: p = parser.points[name] self.points_table.setItem(row, 2, QTableWidgetItem(f"{p.east:.4f}" if p.east else "")) @@ -1161,7 +1347,6 @@ class NetworkAdjustmentTab(QWidget): QMessageBox.warning(self, "Fehler", "Bitte zuerst eine JXL-Datei laden!") return - # Automatische Erkennung falls noch nicht geschehen if not self.fixed_points and not self.measurement_points: self.auto_detect_points() @@ -1169,30 +1354,25 @@ class NetworkAdjustmentTab(QWidget): QMessageBox.warning(self, "Fehler", "Keine Festpunkte erkannt!") return - # Adjustment erstellen self.adjustment = NetworkAdjustment(self.main_window.parser) self.adjustment.max_iterations = self.max_iter_spin.value() self.adjustment.convergence_limit = self.convergence_spin.value() / 1000.0 self.adjustment.sigma_0_priori = self.sigma0_spin.value() - # Beobachtungen extrahieren self.adjustment.extract_observations() self.adjustment.initialize_points() - # Festpunkte setzen (automatisch erkannte) for point_name in self.fixed_points: self.adjustment.set_fixed_point(point_name) try: result = self.adjustment.adjust() - - # Bericht erstellen mit Festpunkt/Messpunkt-Unterscheidung report = self.create_detailed_report() self.results_text.setText(report) status = "konvergiert" if result.converged else "nicht konvergiert" self.main_window.statusBar().showMessage( - f"Ausgleichung abgeschlossen ({status}, {result.iterations} Iterationen)") + f"Ausgleichung {status}, {result.iterations} Iterationen") except Exception as e: QMessageBox.critical(self, "Fehler", f"Ausgleichung fehlgeschlagen: {e}") @@ -1200,70 +1380,36 @@ class NetworkAdjustmentTab(QWidget): traceback.print_exc() def create_detailed_report(self): - """Erstellt einen detaillierten Bericht mit Festpunkt/Messpunkt-Unterscheidung""" if not self.adjustment or not self.adjustment.result: - return "Keine Ergebnisse vorhanden." + return "Keine Ergebnisse." lines = [] lines.append("=" * 80) lines.append("NETZAUSGLEICHUNG - ERGEBNISBERICHT") lines.append("=" * 80) lines.append("") - - # Allgemeine Informationen - lines.append("ALLGEMEINE INFORMATIONEN") - lines.append("-" * 80) - lines.append(f"Job: {self.main_window.parser.job_name}") - lines.append(f"Anzahl Festpunkte: {len(self.fixed_points)}") - lines.append(f"Anzahl Messpunkte: {len(self.measurement_points)}") - lines.append(f"Anzahl Beobachtungen: {self.adjustment.result.num_observations}") - lines.append(f"Iterationen: {self.adjustment.result.iterations}") - lines.append(f"Konvergiert: {'Ja' if self.adjustment.result.converged else 'Nein'}") + lines.append(f"Festpunkte: {len(self.fixed_points)}") + lines.append(f"Messpunkte: {len(self.measurement_points)}") + lines.append(f"Beobachtungen: {self.adjustment.result.num_observations}") + lines.append(f"Iterationen: {self.adjustment.result.iterations}") + lines.append(f"Konvergiert: {'Ja' if self.adjustment.result.converged else 'Nein'}") + lines.append(f"Sigma-0 a-post.: {self.adjustment.result.sigma_0_posteriori:.4f}") lines.append("") - - # Qualitätsparameter - lines.append("GLOBALE QUALITÄTSPARAMETER") lines.append("-" * 80) - lines.append(f"Sigma-0 a-posteriori: {self.adjustment.result.sigma_0_posteriori:.4f}") - lines.append(f"RMSE Richtungen: {self.adjustment.result.rmse_directions:.2f} mgon") - lines.append(f"RMSE Strecken: {self.adjustment.result.rmse_distances:.2f} mm") - lines.append("") - - # Festpunkte - lines.append("FESTPUNKTE (Stationspunkte und Anschlusspunkte)") - lines.append("-" * 80) - lines.append(f"{'Punkt':<12} {'X [m]':>14} {'Y [m]':>14} {'Z [m]':>12} {'σX [mm]':>10} {'σY [mm]':>10}") + lines.append(f"{'Punkt':<12} {'Typ':<12} {'X [m]':>14} {'Y [m]':>14} {'Z [m]':>12}") lines.append("-" * 80) - for name in sorted(self.fixed_points): + for name in sorted(list(self.fixed_points) + list(self.measurement_points)): if name in self.adjustment.points: p = self.adjustment.points[name] - std_x = p.std_x * 1000 if p.std_x else 0 - std_y = p.std_y * 1000 if p.std_y else 0 - lines.append(f"{name:<12} {p.x:>14.4f} {p.y:>14.4f} {p.z:>12.4f} {std_x:>10.2f} {std_y:>10.2f}") - lines.append("") - - # Messpunkte - lines.append("MESSPUNKTE (3000er Serie)") - lines.append("-" * 80) - lines.append(f"{'Punkt':<12} {'X [m]':>14} {'Y [m]':>14} {'Z [m]':>12} {'σX [mm]':>10} {'σY [mm]':>10}") - lines.append("-" * 80) - - for name in sorted(self.measurement_points): - if name in self.adjustment.points: - p = self.adjustment.points[name] - std_x = p.std_x * 1000 if p.std_x else 0 - std_y = p.std_y * 1000 if p.std_y else 0 - lines.append(f"{name:<12} {p.x:>14.4f} {p.y:>14.4f} {p.z:>12.4f} {std_x:>10.2f} {std_y:>10.2f}") - - lines.append("") - lines.append("=" * 80) + typ = "Festpunkt" if name in self.fixed_points else "Messpunkt" + lines.append(f"{name:<12} {typ:<12} {p.x:>14.4f} {p.y:>14.4f} {p.z:>12.4f}") return "\n".join(lines) def export_report(self): - if not self.adjustment or not self.adjustment.result: - QMessageBox.warning(self, "Fehler", "Keine Ergebnisse vorhanden!") + if not self.adjustment: + QMessageBox.warning(self, "Fehler", "Keine Ergebnisse!") return file_path, _ = QFileDialog.getSaveFileName( @@ -1271,35 +1417,23 @@ class NetworkAdjustmentTab(QWidget): if file_path: with open(file_path, 'w', encoding='utf-8') as f: f.write(self.create_detailed_report()) - QMessageBox.information(self, "Erfolg", f"Bericht gespeichert: {file_path}") + QMessageBox.information(self, "Erfolg", f"Gespeichert: {file_path}") def export_points(self): if not self.adjustment or not self.adjustment.result: - QMessageBox.warning(self, "Fehler", "Keine Ergebnisse vorhanden!") + QMessageBox.warning(self, "Fehler", "Keine Ergebnisse!") return - file_path, _ = QFileDialog.getSaveFileName( - self, "Koordinaten speichern", "", "CSV Files (*.csv)") - if file_path: - lines = ["Punkt;Typ;X;Y;Z;Sigma_X;Sigma_Y"] - - for name, p in sorted(self.adjustment.points.items()): - if name in self.fixed_points: - typ = "Festpunkt" - elif name in self.measurement_points: - typ = "Messpunkt" - else: - typ = "Sonstig" - - std_x = p.std_x * 1000 if p.std_x else 0 - std_y = p.std_y * 1000 if p.std_y else 0 - lines.append(f"{name};{typ};{p.x:.4f};{p.y:.4f};{p.z:.4f};{std_x:.2f};{std_y:.2f}") - - with open(file_path, 'w', encoding='utf-8') as f: - f.write("\n".join(lines)) - QMessageBox.information(self, "Erfolg", f"Koordinaten gespeichert: {file_path}") + points = [] + for name, p in self.adjustment.points.items(): + points.append(CORPoint(name=name, x=p.x, y=p.y, z=p.z)) + + export_points_with_dialog(self, points, "ausgeglichen") +# ============================================================================= +# Referenzpunkt-Anpassung Tab +# ============================================================================= class ReferencePointAdjusterTab(QWidget): """Tab für Referenzpunkt-Anpassung""" @@ -1334,8 +1468,8 @@ class ReferencePointAdjusterTab(QWidget): layout.addWidget(info_group) - # Neue Koordinaten Gruppe - new_coords_group = QGroupBox("Neue Koordinaten für Referenzpunkt") + # Neue Koordinaten + new_coords_group = QGroupBox("Neue Koordinaten") new_coords_layout = QGridLayout(new_coords_group) new_coords_layout.addWidget(QLabel("East (X):"), 0, 0) @@ -1359,13 +1493,10 @@ class ReferencePointAdjusterTab(QWidget): self.new_elev_spin.setSuffix(" m") new_coords_layout.addWidget(self.new_elev_spin, 2, 1) - # Translation anzeigen - new_coords_layout.addWidget(QLabel(""), 3, 0) self.delta_label = QLabel("ΔX: 0.000 m | ΔY: 0.000 m | ΔZ: 0.000 m") self.delta_label.setStyleSheet("color: blue;") - new_coords_layout.addWidget(self.delta_label, 4, 0, 1, 2) + new_coords_layout.addWidget(self.delta_label, 3, 0, 1, 2) - # Koordinaten-Änderungen live berechnen self.new_east_spin.valueChanged.connect(self.update_delta) self.new_north_spin.valueChanged.connect(self.update_delta) self.new_elev_spin.valueChanged.connect(self.update_delta) @@ -1376,25 +1507,25 @@ class ReferencePointAdjusterTab(QWidget): actions_group = QGroupBox("Aktionen") actions_layout = QHBoxLayout(actions_group) - preview_btn = QPushButton("Vorschau berechnen") + preview_btn = QPushButton("Vorschau") preview_btn.clicked.connect(self.preview_transformation) preview_btn.setStyleSheet("background-color: #4CAF50; color: white;") actions_layout.addWidget(preview_btn) - apply_btn = QPushButton("Transformation anwenden") + apply_btn = QPushButton("Anwenden") apply_btn.clicked.connect(self.apply_transformation) apply_btn.setStyleSheet("background-color: #2196F3; color: white;") actions_layout.addWidget(apply_btn) - export_btn = QPushButton("Neue JXL exportieren") - export_btn.clicked.connect(self.export_jxl) + export_btn = QPushButton("📥 Exportieren...") + export_btn.clicked.connect(self.export_points) export_btn.setStyleSheet("background-color: #FF9800; color: white;") actions_layout.addWidget(export_btn) layout.addWidget(actions_group) # Vorschau-Tabelle - preview_group = QGroupBox("Vorschau der betroffenen Punkte") + preview_group = QGroupBox("Vorschau") preview_layout = QVBoxLayout(preview_group) self.preview_table = QTableWidget() @@ -1407,29 +1538,27 @@ class ReferencePointAdjusterTab(QWidget): layout.addWidget(preview_group) # Bericht - report_group = QGroupBox("Transformationsbericht") + report_group = QGroupBox("Bericht") report_layout = QVBoxLayout(report_group) self.report_text = QTextEdit() self.report_text.setReadOnly(True) self.report_text.setFont(QFont("Courier", 9)) + self.report_text.setMaximumHeight(150) report_layout.addWidget(self.report_text) layout.addWidget(report_group) def load_reference_point(self): - """Lädt die Informationen zum Referenzpunkt""" if not self.main_window.parser: - QMessageBox.warning(self, "Fehler", - "Bitte zuerst eine JXL-Datei im 'JXL-Analyse' Tab laden!") + QMessageBox.warning(self, "Fehler", "Bitte zuerst JXL laden!") return self.adjuster.set_parser(self.main_window.parser) info = self.adjuster.get_reference_point_info() if not info["found"]: - QMessageBox.warning(self, "Fehler", - f"Referenzpunkt nicht gefunden: {info['message']}") + QMessageBox.warning(self, "Fehler", info['message']) return self.ref_point_name.setText(info["name"]) @@ -1437,16 +1566,11 @@ class ReferencePointAdjusterTab(QWidget): self.current_north.setText(f"{info['north']:.4f} m") self.current_elev.setText(f"{info['elevation']:.4f} m") - # Setze aktuelle Werte als Standard für neue Koordinaten self.new_east_spin.setValue(info['east']) self.new_north_spin.setValue(info['north']) self.new_elev_spin.setValue(info['elevation']) - - self.main_window.statusBar().showMessage( - f"Referenzpunkt '{info['name']}' geladen") def update_delta(self): - """Aktualisiert die Anzeige der Verschiebung""" if not self.adjuster.parser: return @@ -1454,17 +1578,13 @@ class ReferencePointAdjusterTab(QWidget): dy = self.new_north_spin.value() - self.adjuster.original_coords[1] dz = self.new_elev_spin.value() - self.adjuster.original_coords[2] - self.delta_label.setText( - f"ΔX: {dx:+.4f} m | ΔY: {dy:+.4f} m | ΔZ: {dz:+.4f} m") + self.delta_label.setText(f"ΔX: {dx:+.4f} m | ΔY: {dy:+.4f} m | ΔZ: {dz:+.4f} m") def preview_transformation(self): - """Zeigt eine Vorschau der Transformation""" if not self.main_window.parser: - QMessageBox.warning(self, "Fehler", - "Bitte zuerst eine JXL-Datei laden!") + QMessageBox.warning(self, "Fehler", "Bitte zuerst JXL laden!") return - # Validierung valid, message = self.adjuster.validate_input( self.new_east_spin.value(), self.new_north_spin.value(), @@ -1473,7 +1593,7 @@ class ReferencePointAdjusterTab(QWidget): if not valid: reply = QMessageBox.warning(self, "Warnung", - f"Mögliche Probleme erkannt:\n\n{message}\n\nTrotzdem fortfahren?", + f"{message}\n\nTrotzdem fortfahren?", QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.No: return @@ -1486,7 +1606,6 @@ class ReferencePointAdjusterTab(QWidget): results = self.adjuster.preview_transformation() - # Tabelle aktualisieren self.preview_table.setRowCount(len(results)) for i, result in enumerate(results): self.preview_table.setItem(i, 0, QTableWidgetItem(result.original_point)) @@ -1497,148 +1616,109 @@ class ReferencePointAdjusterTab(QWidget): self.preview_table.setItem(i, 5, QTableWidgetItem(f"{result.new_coords[1]:.4f}")) self.preview_table.setItem(i, 6, QTableWidgetItem(f"{result.new_coords[2]:.4f}")) - # Bericht aktualisieren self.report_text.setText(self.adjuster.get_summary_report()) - - self.main_window.statusBar().showMessage( - f"Vorschau berechnet: {len(results)} Punkte betroffen") def apply_transformation(self): - """Wendet die Transformation auf die JXL-Datei an""" if not self.main_window.parser: - QMessageBox.warning(self, "Fehler", - "Bitte zuerst eine JXL-Datei laden!") + QMessageBox.warning(self, "Fehler", "Bitte zuerst JXL laden!") return if not self.adjuster.affected_points: - QMessageBox.warning(self, "Fehler", - "Bitte zuerst eine Vorschau berechnen!") + QMessageBox.warning(self, "Fehler", "Bitte zuerst Vorschau berechnen!") return reply = QMessageBox.question(self, "Bestätigung", - f"Soll die Transformation auf {len(self.adjuster.affected_points)} Punkte angewendet werden?\n\n" - "Die Änderungen werden in der geladenen JXL-Datei gespeichert.", + f"Transformation auf {len(self.adjuster.affected_points)} Punkte anwenden?", QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.No: return if self.adjuster.apply_transformation(): - # Bericht aktualisieren self.report_text.setText(self.adjuster.get_summary_report()) - # JXL-Tab aktualisieren jxl_tab = self.main_window.tabs.widget(0) if hasattr(jxl_tab, 'update_display'): jxl_tab.update_display() - QMessageBox.information(self, "Erfolg", - "Transformation erfolgreich angewendet!\n\n" - "Die Koordinaten wurden aktualisiert.\n" - "Verwenden Sie 'Neue JXL exportieren' zum Speichern.") - - self.main_window.statusBar().showMessage("Transformation angewendet") - else: - QMessageBox.critical(self, "Fehler", - "Transformation konnte nicht angewendet werden!") + QMessageBox.information(self, "Erfolg", "Transformation angewendet!") - def export_jxl(self): - """Exportiert die modifizierte JXL-Datei""" + def export_points(self): if not self.main_window.parser: - QMessageBox.warning(self, "Fehler", - "Bitte zuerst eine JXL-Datei laden!") + QMessageBox.warning(self, "Fehler", "Keine Punkte!") return - if not self.adjuster.transformation_applied: - reply = QMessageBox.warning(self, "Warnung", - "Die Transformation wurde noch nicht angewendet.\n" - "Möchten Sie die Originaldatei exportieren?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.No: - return + points = [] + for name, p in self.main_window.parser.get_active_points().items(): + if p.east is not None and p.north is not None: + points.append(CORPoint(name=name, x=p.east, y=p.north, z=p.elevation or 0)) - # Standard-Dateiname vorschlagen - original_path = self.main_window.parser.file_path - if original_path: - import os - base, ext = os.path.splitext(original_path) - default_name = f"{base}_transformed{ext}" - else: - default_name = "transformed.jxl" - - file_path, _ = QFileDialog.getSaveFileName( - self, "JXL-Datei exportieren", default_name, "JXL Files (*.jxl)") - - if file_path: - if self.adjuster.export_jxl(file_path): - QMessageBox.information(self, "Erfolg", - f"JXL-Datei exportiert:\n{file_path}") - self.main_window.statusBar().showMessage(f"Exportiert: {file_path}") - else: - QMessageBox.critical(self, "Fehler", - "Fehler beim Exportieren der JXL-Datei!") + export_points_with_dialog(self, points, "angepasst") +# ============================================================================= +# Hauptfenster +# ============================================================================= class MainWindow(QMainWindow): """Hauptfenster der Anwendung""" def __init__(self): super().__init__() self.parser = None - self.setWindowTitle("Trimble Geodesy Tool v2.0") - self.setMinimumSize(1000, 700) + self.setWindowTitle("Trimble Geodesy Tool v2.1") + self.setMinimumSize(1100, 800) self.setup_ui() self.setup_menu() def setup_ui(self): - # Zentrales Widget central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) # Tab-Widget self.tabs = QTabWidget() - - self.tabs.addTab(JXLAnalysisTab(self), "📁 JXL-Analyse") + self.tabs.addTab(JXLAnalysisTab(self), "📊 JXL-Analyse") self.tabs.addTab(CORGeneratorTab(self), "📄 COR-Generator") self.tabs.addTab(TransformationTab(self), "🔄 Transformation") self.tabs.addTab(GeoreferencingTab(self), "🌍 Georeferenzierung") self.tabs.addTab(NetworkAdjustmentTab(self), "📐 Netzausgleichung") - self.tabs.addTab(ReferencePointAdjusterTab(self), "📍 Referenzpunkt-Anpassung") + self.tabs.addTab(ReferencePointAdjusterTab(self), "📍 Referenzpunkt") main_layout.addWidget(self.tabs) - # Statusleiste + # Status Bar + self.setStatusBar(QStatusBar()) self.statusBar().showMessage("Bereit - Bitte JXL-Datei laden") def setup_menu(self): menubar = self.menuBar() # Datei-Menü - file_menu = menubar.addMenu("&Datei") + file_menu = menubar.addMenu("Datei") - open_action = QAction("&Öffnen...", self) + open_action = QAction("JXL öffnen...", self) open_action.setShortcut("Ctrl+O") - open_action.triggered.connect(self.open_file) + open_action.triggered.connect(self.open_jxl_file) file_menu.addAction(open_action) file_menu.addSeparator() - exit_action = QAction("&Beenden", self) + exit_action = QAction("Beenden", self) exit_action.setShortcut("Ctrl+Q") exit_action.triggered.connect(self.close) file_menu.addAction(exit_action) # Hilfe-Menü - help_menu = menubar.addMenu("&Hilfe") + help_menu = menubar.addMenu("Hilfe") - about_action = QAction("&Über...", self) + about_action = QAction("Über...", self) about_action.triggered.connect(self.show_about) help_menu.addAction(about_action) - def open_file(self): + def open_jxl_file(self): file_path, _ = QFileDialog.getOpenFileName( self, "JXL-Datei öffnen", "", "JXL Files (*.jxl);;All Files (*)") + if file_path: jxl_tab = self.tabs.widget(0) jxl_tab.file_path_edit.setText(file_path) @@ -1646,27 +1726,36 @@ class MainWindow(QMainWindow): def show_about(self): QMessageBox.about(self, "Über Trimble Geodesy Tool", - "
Geodätische Vermessungsarbeiten mit JXL-Dateien
" - "Funktionen:
" - "Version 2.0 - Überarbeitet Januar 2026
") + "Trimble Geodesy Tool v2.1\n\n" + "Geodätische Vermessungsarbeiten mit JXL-Dateien\n\n" + "Features:\n" + "• JXL-Datei Analyse mit TreeView\n" + "• COR/CSV/TXT/DXF Export\n" + "• Koordinatentransformation\n" + "• Georeferenzierung\n" + "• Netzausgleichung\n" + "• Referenzpunkt-Anpassung") def main(): app = QApplication(sys.argv) + app.setStyle("Fusion") - # Modernes Styling - app.setStyle('Fusion') + # Dunkleres Theme + palette = QPalette() + palette.setColor(QPalette.Window, QColor(240, 240, 240)) + palette.setColor(QPalette.WindowText, QColor(0, 0, 0)) + palette.setColor(QPalette.Base, QColor(255, 255, 255)) + palette.setColor(QPalette.AlternateBase, QColor(245, 245, 245)) + palette.setColor(QPalette.ToolTipBase, QColor(255, 255, 220)) + palette.setColor(QPalette.ToolTipText, QColor(0, 0, 0)) + palette.setColor(QPalette.Text, QColor(0, 0, 0)) + palette.setColor(QPalette.Button, QColor(240, 240, 240)) + palette.setColor(QPalette.ButtonText, QColor(0, 0, 0)) + palette.setColor(QPalette.BrightText, QColor(255, 0, 0)) + palette.setColor(QPalette.Highlight, QColor(42, 130, 218)) + palette.setColor(QPalette.HighlightedText, QColor(255, 255, 255)) + app.setPalette(palette) window = MainWindow() window.show() diff --git a/modules/__pycache__/cor_generator.cpython-311.pyc b/modules/__pycache__/cor_generator.cpython-311.pyc index a474397..c9e2e49 100644 Binary files a/modules/__pycache__/cor_generator.cpython-311.pyc and b/modules/__pycache__/cor_generator.cpython-311.pyc differ diff --git a/modules/__pycache__/transformation.cpython-311.pyc b/modules/__pycache__/transformation.cpython-311.pyc index 2476ea5..b332504 100644 Binary files a/modules/__pycache__/transformation.cpython-311.pyc and b/modules/__pycache__/transformation.cpython-311.pyc differ diff --git a/modules/cor_generator.py b/modules/cor_generator.py index 002a939..d9e0d25 100644 --- a/modules/cor_generator.py +++ b/modules/cor_generator.py @@ -259,42 +259,16 @@ class CORGenerator: return self.cor_points - def write_cor_file(self, output_path: str, include_header: bool = True) -> str: - """Schreibt die COR-Datei""" + def write_cor_file(self, output_path: str, include_header: bool = False) -> str: + """ + Schreibt die COR-Datei + Format: PunktID,X,Y,Z (Komma-getrennt, KEINE Header-Zeile) + """ lines = [] - # Sammle eindeutige Stationsstarts für Header - ref_line = self.parser.get_reference_line() - stations_sorted = sorted(self.parser.stations.items(), - key=lambda x: x[1].timestamp) - - current_station_idx = 0 - written_points = set() - - for station_id, station in stations_sorted: - # Header für neue Station (Referenzlinie) - if station.station_type == 'ReflineStationSetup' and ref_line: - if include_header: - # Markdown-Style Header - lines.append(f"|{ref_line.start_point} |0.000 |0.000.1 |0.000.2 |") - lines.append("|----:|----:|----:|----:|") - - # Punkte von dieser Station - measurements = self.parser.get_measurements_from_station(station_id) - for meas in measurements: - if meas.name and meas.name not in written_points: - # Finde den COR-Punkt - for cp in self.cor_points: - if cp.name == meas.name: - lines.append(cp.to_cor_line()) - written_points.add(meas.name) - break - - # Verbleibende Punkte for cp in self.cor_points: - if cp.name not in written_points: - lines.append(cp.to_cor_line()) - written_points.add(cp.name) + # Format: Name,X,Y,Z (ohne Header, Komma als Trenner) + lines.append(f"{cp.name},{cp.x:.4f},{cp.y:.4f},{cp.z:.4f}") content = "\n".join(lines) @@ -304,11 +278,15 @@ class CORGenerator: return content def export_csv(self, output_path: str) -> str: - """Exportiert als CSV-Datei""" - lines = ["Punktname;East;North;Elevation"] + """ + Exportiert als CSV-Datei + Format: PunktID,X,Y,Z (Komma-getrennt, KEINE Header-Zeile) + """ + lines = [] for cp in self.cor_points: - lines.append(f"{cp.name};{cp.x:.4f};{cp.y:.4f};{cp.z:.4f}") + # Format: Name,X,Y,Z (ohne Header, Komma als Trenner) + lines.append(f"{cp.name},{cp.x:.4f},{cp.y:.4f},{cp.z:.4f}") content = "\n".join(lines) diff --git a/modules/transformation.py b/modules/transformation.py index e0da406..10ffac0 100644 --- a/modules/transformation.py +++ b/modules/transformation.py @@ -108,6 +108,42 @@ class CoordinateTransformer: return True + def compute_translation_only(self, + xy_origin_name: str, + z_reference_name: Optional[str] = None) -> bool: + """ + Berechnet nur Translation (KEINE Rotation): + - xy_origin_name: Punkt wird zum Ursprung (0,0) + - z_reference_name: Punkt definiert Z=0 + """ + # Finde Punkte + xy_origin = None + z_ref = None + + for p in self.original_points: + if p.name == xy_origin_name: + xy_origin = p + if z_reference_name and p.name == z_reference_name: + z_ref = p + + if xy_origin is None: + return False + + # Nur Translation, keine Rotation + self.params.dx = -xy_origin.x + self.params.dy = -xy_origin.y + self.params.rotation_gon = 0.0 + self.params.pivot_x = 0.0 + self.params.pivot_y = 0.0 + + # Z-Verschiebung + if z_ref: + self.params.dz = -z_ref.z + else: + self.params.dz = -xy_origin.z + + return True + def transform(self) -> List[CORPoint]: """Führt die Transformation durch""" self.transformed_points = []