From 3d6eb10edb726987304bcfad6f4d0ff6af109d7c Mon Sep 17 00:00:00 2001 From: Developer Date: Sun, 18 Jan 2026 20:59:22 +0000 Subject: [PATCH] =?UTF-8?q?Version=203.0:=20=C3=9Cberarbeitung=20mit=20Ber?= =?UTF-8?q?echnungsprotokoll,=20Datenfluss,=20korrektem=20Netzausgleichung?= =?UTF-8?q?skonzept?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Änderungen: 1. JXL-Analyse: - TreeView zeigt Stationen mit allen Messungen korrekt an - Hz, V, Distanz, Prismenkonstante werden angezeigt - Anschlussmessungen separat markiert - Neues Berechnungsprotokoll mit Export (TXT/PDF) 2. Georeferenzierung: - Automatische Punktzuordnung über Tripel-Analyse - Button 'Automatische Zuordnung' hinzugefügt - Option für ausgeglichene Punkte 3. COR Generator: - Nur ComputedGrid-Methode (korrekte Werte) - Option für ausgeglichene Punkte 4. Datenfluss zwischen Modulen: - Globaler Speicher (AdjustedPointsStore) - Button 'Ausgeglichene Punkte übernehmen' - Status-Anzeige in GUI 5. Netzausgleichung: - KORREKTES KONZEPT implementiert: * Festpunkte = Passpunkte (5001, 5002) * Neupunkte = Standpunkte (werden ausgeglichen) * Messpunkte = Detailpunkte (werden ausgeglichen) - Klare Unterscheidung in GUI 6. Tests mit Beispieldatei bestanden: - 84/84 Punkte stimmen mit COR-Referenz überein --- .abacus.donotdelete | 2 +- main.py | 1117 +++++++++++------ .../__pycache__/jxl_parser.cpython-311.pyc | Bin 32226 -> 51901 bytes modules/jxl_parser.py | 353 +++++- test_data/berechnungsprotokoll_test.txt | 765 +++++++++++ test_with_example.py | 264 ++++ 6 files changed, 2083 insertions(+), 418 deletions(-) create mode 100644 test_data/berechnungsprotokoll_test.txt create mode 100644 test_with_example.py diff --git a/.abacus.donotdelete b/.abacus.donotdelete index c759120..3e2f036 100644 --- a/.abacus.donotdelete +++ b/.abacus.donotdelete @@ -1 +1 @@ -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 +gAAAAABpbOCKPm7TZmncaVQrhP1K8-PoepDfLIiWTSSy6rgbWdGofEzxyK0DUZpdLz5ymgdqehcC2MUENAwoBL7b-0UBZVkcIpeSnsLRiRQ4-Ve3gbowiSiu4E9QnmLToYpCPBKnvmlO3oJUXJ2ti_CZ9_M1Xru-M1eYj3Z545NmAo4URY6dgzndR836JoS2gSJbHDLHe7h0PCZk7hfCY0nFP9hELsTobV-E8dQiAgJqVFjoMfVwEo3YHPon3bhLcTY9IswcM0YXncLC4yM3WZus9_qLc4l0QNXZWkxWhVEAoC21OQNCsCx0niK9NSQ6ZHKCCGlWOvGO69fbkzZp1QzSfZVuguoRW8knWznvvGXAzJ0R3mGFNshxLUdBW2kvvLFnONUZAJ8PjOUZXow2KZVPSRve7G9OMaKXpjv75blxZ2iusfuvm0YPlBBwVWGn8zPTApKG35QOBv_w5swXjCoOCQzjZxEQfU6VgUA9-S70GoGQgyyjvp5cVtNamlsnzihsoSojaoRNBJEpkwB5qIaRXG4TqeohKxC6qtNeirQjbTLHqHdgrA-zTXN4ybE4ZndvFaT_IB4yJR_7a5mKIS1Nn91XCNVeqN2jyZIq4BQGflSk8i4NMqk37E2_VHyeGuorYLcqWodvHbeE9dUyvvwMHgbQ5jVBLv2qBEDQ31BpukhJvcmOk4NDoCq6Z8Z7gU92UaSMbBLcqxh0nGi--J-iDpU5ATiG_xSQriGWaaxSaEKinG8D4_MhBxFaXieMrqgh7wPAQAILAVLVxHjTeGAFjXKjvAjcDF_ND61AZrZeAVLiuSwFeVI3IDw_c3nltlj0NHW9eDblYKm_b-XDXteLqwKne8VGLQJTziv49xfQOqGE6n6NDiUCPD8avBPOvAw_WthtY0NRc5juAHk0uF-fc3POk1kCabpLc7JLTe0Wos-e4o6DMkLa6RxEhyQhdTsuKvykqpE94FFzzcIe-MZCS5lbrUEXlALhNIys_nYiHvmhmyD6tTIrFBc7ATp5wmCxDEqz2Tkw18bVxml0H2HlYDB_cn4CQNuuJqSGDUwSEEHjM-7mWv_Bhnf4oGjZj3zzQOJPdDa0NTVM0ZMIYB0dl6owmY4UVtHeKM249u2muKqTGghla2Oz_Xm-R0PfWheEK9sNfJyKr5QkEHl-Yqc1GY6x03FNjzasMDeFb_b3Hod0blTwbUm0BQTsVfZdX8kYG-U9B6fTNhMxpnpKQKy0XWa5STDBFoBcd69qJr-hYkMb34cgRvXHREOKGlSDlesvl3EN12HH6lDK20iUsP09ALZTzoq6yLEd_Yb9Ch721Zs8Ye5vN4e-nYF6kcrZ6vx5KIoVdHkH19DfKWs2Ypm8xDLzxZno1k4RrwI9B6i-GLONxfdPBg-Oq5uKwHqI2XHEFQ4UBlaUoAzKgnD203AYzu6cz9iSUx089jkQba2mFrcK4y-yLRjOcOCWcNqCEmBCM4fAcZljer8md_KgJ0GGq2_YbelODwSyn2VmA5WJAnyOWfvks-Pv-5rWSE-vry159-t0xR5M5joD_iR5P2tK_FB4Xf8wIVKB5_6R8Ho9HaqXMJqr3jPkRS23MPUICP_3q9mg3N98bO9ob_i1F918Jb4BQcF81AsUHpTo1vXUitSI5rAsEH1KQrT_gTsD8aYM_r4kRlUavp3Vloj143gPqOlZgYGJG8npzJH-AltDbewTNA48TuzBWXTiCSOkG-XL5UeSEOY-74Zf1K3w9pSEtu9sJjNIegcgNej17Sx9vP7aJpLRT-01WSPVpVozmf-pJV2fnAaChjSt5HHjo5BkBHSJ-_9Mmh9RG9d9Cb3uMBsH0wfVxhBPCXcsYFs6NhUcEASl-uDI2vrVcDZNi2rMRpTaLzppOeE0hRO7IordCi_WRkdS-JFHZlceTXhjfeyLQD922ge2i7d1QDBDd2qVgewEYegdKvOorsQo0hqUEUjsmxz5sxo5B2Cy7XMAPmMxI05jr2IA7P4Qw2iHV5B2-6EZPxj_VdQKm6sIjhl-sySOWp1iii-NHtk5QvnfhsHqBMUgw23cPpdioMo0TVFpH7tSPY0cmDGwhFcTmqQiyoi8H3cItX6lT3rblLZvBtLbC4UdENd0YP8SrlWN_k8Zn3u9s9ArPhSP2ZQ7bK0ocLTTkk1uvlqYNd8k_cnxCrEibUjXvfeTMKFOTXD774tlYKpr9dcva16NSfpz1siVCAiN8bWm5qDhYr6wFHAmzSRz8IkAVp6J5g7aWGXjyjCkMiLMPVkE86b7h9FdPlImKj-_yfDSQcxZ6vTefYI22zmp6GNEi1LbcecAYerC4ZcwbahcvU4NLf8SZRH6OaecpO8Dg0VF8uCfMqapwX6ViJInzK5_7bvnlTJZmL5cLfD3Bupijto6rNeeYLfdrlptHQm3Gae4dCasKY1eXtZLi5Wk-L99bgwBuNdM4br-uaa3vYotnvXCsSfpCBo1L5vWLKd8bu_FNCm2gqSRkw6ioSLfDgSwSHwJ-M6rwUyLEL5LjUizGc2rSI_J6glKLt43U2hq5YQy0g1E7J7pW4u_-sZnAnTI5C0eFtpC9cwJW1MjyT3cX5v7gPAKBGJ1LSiFmIXgeA28ibi4XzCHqSOpFFCRFJfnnYFuFermrpvRNOtXr7TdpCf4OxYteceG0Yw250Cd7qyCzdlCKUiagX8SogXGTRqElUo83fdY8YLeQkhxIYKhbJByM3jyFg3QCuv7mFqK7Q0NMuwk4W8GNX_akBHwSdOelBL6ZwddXXM7PjRXJUJ17zkiE1u0Y0MT6BgB_KAmgNlWt7gGKRomj2BGz0AsNWqiUzX2WMf_3lWPsgSXZr6gyEkUFJvgUdB3_LUvS8cCH3HzpaxfeDukSuvzPbxD4svKRtkVUTje9NJtEbWpDr8ZZTebrloWguJiQPMrKJ0M_i7SlWThi1EgbhdD7159MuVVPdA18FInY6HL0GFBFBj9XI7h9WELW_74rWjlw9Nz5a0ZXD9RY53yEsq-gX0fssc2vKwtqEv96t62MYJ54teMD6NaIOYh8lnHVUz5dVm466ijd3GTZlszZuLaDIbDPlRwcAifEQpAZRwt-B5Fau9pyMw-GUXeFVJIKkP2OUSL3_Ucko4WmcujIhm0Uv21u02Yt-h_ZkWKXegRYfwClng39HkmllAw7Lezm8poqlcdQH-E0WdGS6pD1h4T193lUQ2KIe4mia2P8Zh7Sb1nUbb3MifConw5xFqvww3K2VJpkL5CIExkGrxML5leZe8tNDL3477y2wdckdZLtH2GCndn6JB97fljgLu7r5Z01GsbOP7QfFuymXRbB5QsKrhzPdebc3DrNJLUjxxNrvqOffRh8Q3lnjMmhiqGUlRtXWaaZG5zfJdbIqCm5ifxG25cwtg7sNAWjAPgxeD1FmOPu3znp2W-zmbh0ohW2PNmMVeo8h3APjTqCU5GKs6kAKAmAB8QquoEYip4raNfRTS3nmSLthNSDpgxr-8KEuOL71Lg_rPm3r0IY-C4VVAcbFb1wCJvtwU8sNiGDIMhxaH0pA3YGdwhN_5-8AAIN1d6F94Yo6VJgRJ-ZzK4QDYrSHESjuG9S4WBsPuSPjqjMPhAUCqw1yu5R631A04_-tdrNEpgDISAsiuMDPxE5FMyYQjnEea5H8X6L_2AuAHkOAAKXXsYXxXRhqaxj1GSRcAKc4LUaO-iMniP1rSG_S9dxBdtJjxTOCQJaYIIlz1aOCPad3U18jQOWaB9FIuJwAa9fZICTlucU7IcZFiTNDy-f9tzwSLROBWaJyDyDeHnwMeroPIMr0qd_RC5OknRWZbkqnTVmd4bhwdM95Kfz5KeyZ5ytf2xF0u58U0X4s6dA3h39sNKVg3qM9IHtSGv7sz2NCaeflyyJZEWNhIlKiUZdX6vM52DEVx1TBY07O0BBPma1t0s-itjL3lZbqsqkgCIfwROyZ81BT4BVaDG31k_aBNfVPDCMWh4ISWX4I_ywJrjeezFD5jpE9t5UslbUY8PFpwkQcquC_DIxkyPEDBomSFnhf8fO55ten0IJAW952JxeTL5vy4htlBDhAz-CkfcKbbMM4W5NlnOZatrqU316Jy1MgDTVCOMyoUGbqibPqUE5RHBEJyiOXlqqGT5ljXTMVvJSNZdP3sv0wwkT1ZMJcUtKJdyjQ-_S8SLU4G7s08pLDSJn0fyFI7gqXQHaj9KEP-_lv1WkV343-mSr7Rb1Puf8JEc_VefkZe4BjHGrXt4cOCmfBb27cRoHJEtyCME8-e2cFu3kGHjJQ-XYLywaYLtLFrZlHxL2_EiJMS1u8EGxXJi_cgZ5OVMS3OlxasxUMN1qtoqHorIoXxIdpjy1OjvximQ-EFrfj_gCkd408moenFDHKRlnBcKCacvUmbTESsVNvcK-zXa_4eIo6Jbsosh-2r-qUz86P1Z9atX6Cs4gOaLy6nV8nR8PUqOup70MzY9YyLf8BCKous-ltsyDLrXLTfa9SKLboCgAfaelVPg0DzTFk4j9oq-8RKKWI4koBYN6P0AQjbzPow5rGhv-NNkcFf_s77fnbHkIMLN50A8eZ3fFkMka00Rr66N601BWEbPmqbkmz4XHyDk0SJSZ-xJll8FdBW_RdDC3n14xe03nRJbUZF0I1ufzcXDFFJeSzNIDIF-VacrJM4Rmh-Ak4gw47KcGrIa5XxcFQvb-5g1wGSQZmdQaMs-v9CSiYw1W_yLqLSQI_L_h50K2QL23jSpEPW81El8cyHXJYa7RptJ_BcsFlfkXSH6wSpyOJjyFm-ssxzJ4gbHER2MHbcuEc2lnhvQ77w_ITgjF6yHkuTogh2jCQd-qciLnExOJUt5H0UnWblBN9q39bz4XzgN6h2weaUYQvCoSgCE_5TJqXUDZLlD14an8qIp95QdBT6IgWajxP1jPyIAKCGGrZUrpor4Y3DzbABKMk1TbBpK_YQseZlcpzitCJ4eIVFOqGbBtE7Zs3Xbkt24sNMxlgcJcYbPLvZ6_rpvH-RFQR39N2L4RMz9_zGaL9lUY6RQSEeLGNUHkF6-F9Q8e-MVbc3qEHMjPfSm4BrEUIe8hhaHR53Kk0Vp3YxJ6p7CSSgw_p_HzXlnBEZB_DxgzcHoHlOHegIJi7VQqkefb5LrsTo3EsSUMG7rPZh1jgnlbJVWgVs45hM3mvblKrwn5uWRQ0F6ybmSm_HweO7LO_M0Ilgs6BuST5vvXKz60ZiWhq9148pnSK73E7AKHPKVIxlIxRTyp78UuLudP1HdWWV_FJPwWrSxWXES_IRLXncy2eUjt-Fhq80MwpWlZJHTZyfBZauq8SFx61wiSYohGi6IqgrcbkqsPjH7o99nH4y_LbRlWhybR1lRT1Kv7mj77_qoe6uI-prVHakIcgMaye_avWZFQoIMdRV2wFCZLIpWNM0CCpZzUSnRjd_ZJbf0F-QK3U7_LjKJeTO1-SNsfx4jcMCsFIsMTTCL3Gf4xcXxtwSnDagZ51M23Pa2ZriprfXvt02MAE5KX_e8cP0krFPzyRglCMVCHL4OnV5hfHnuGYdGB-_xRYKds3IpUMPleGZLmvHbvpvWRnP7qlZBz1h8hjBf0G2peAsgGO1L8H227oWV_UzHHVkbnDLhPbPEe-Ym6Rk0-G3KFFl3qq9tZCKaBowKELRvhr7__QQoy-gP10cHeb1vWYFkyqDzjn7CpbHU8d-lljzEhm53bsxtwIbR7JdCTv01mezgKi1K0pIEILt0jIhGCYD2CfeQnX5uqHUdMzCVR8ERgNgjCFQsiI-aVdhigQqUF_u3wlmbTjtsf2UpO5bJuzwLJZ0eHOAnFqMzGXD6ll0a1pNHCE-eCNA7eqtWKvWtIWR-UrgknZGzXkHxsjvt58pOLvYuK3sIeGIoW5b_ZwRK7_SdgD6py1m_xaj8UklJ_zWL4IbI2h3fxuj-Y7ZUh2DqUubkg91iDYumzdsUAHXVbC_u4IDTLwm1nB3eg4W3XhjLZplAOzVft5pLg3rw2sUjWVLCQry-v6AOTYYncrYkce4NhGRdxAKSsd1mRqqaZkyW5TBEJPYMmgfJxG4rjFihJaS6lMAz8UFvhUgtrXxzRc6eEYK-6mb2Drlnu9pahJfJGD1sOpwY2TgjhsC6w0RCZHlbgZci6XIlpgpLzsewCQOQMHrGtZnWmWE8U5UFF2NbnrAjs5Qh_GXsWLLsanpgiZHsdSdRJw25dUDKYli_A-YgcA1YpwG419oOHSTjGClz_AkEv0nIbEcpaZWzImuC9Ra5OYaPm4-O-_eSGTMkb7TESK5lkFIN4z4ZF810H5sJMkcU9l67bgn3W3sXPy9j3fHHmIPBTfMrjw3YggGcO2M1AX54YBDr4m963dHOQK1nV732UxSE5WFd4AUUZB5bytshZENQkBBTdClHC8iykVyo8TWjONWWFM7xBqgKeCE-OcwSNz9AONhCoPrV3UgVhLUbo5r3LImiUHRZbsAGMe67KM30pELUWjExKTeUD_sUeVKHOiVBkOSOhvqASI6N4ujFWGFPgv_4k4no_2en0nPmFmQ2mOkEv9q4IOgEf-vD5YpQgbVeBRXVsbMMM31RW4VGD57ICi7oL5-GdOMvEw9ct84Kt87XsKNLx86nyXfviYC4b-rqQoRrzGEZM7pWmTH8nmC3XqlDoWQjkDWcGbFt3ajpB0cQkdJZrNCAMTsVUBQb4X9dDB9bdvAF3jmyHWgmT61VtBAf9qNcXBOMfkFhnmtnVeP-fbz5EOYRx0dZFrJxMHM5Fxmea5Vh8hZ_rmy8kG8601J1VoAln-rsNnjs48bnYUKGGTOoFAMcgdCGtJD-sqdFgrSnp9L87sbxFsJfHMNLRd74RuGYKNuFYiaVmyJeD7ZX_itQqJqIu-6zxuR50H8BDkKIGKzZIBqFwGLAKZlRzFJwrcDmLfAB7OYFnrbly8s5lJrPd0FSRblL6BVGTZ-XTbsxf40xboLWVzpUZ8kY2GnVsFgO4Qj66S821WdqLuyMEdREamWYKzrHF4SvhjFK4OuhB9KEWgPN6LWap_6lspV-vDHfOFICfgC6pyd602COCcOASMvu7mZB8GF7_aws29D46fr2k-I9bl1NGQa61aVbUtNvP9MHJosglx47ZRmH2WL6YHdd7iZc1BozG2mC-Lb8D0DKzmDCaYuXcNl3trILYSFtJGAGy-ep_CmB1RPA6YgYe3e5QpjOudBJ7FpW-52T30UEQJY1Nq_3YI91L_kFCO0cCqBM-MdjSY04vwUu2-wMEaECO5DZtLFbsPtS-fajZI0EVKsAXgoPAomJLdF4joxsjKNjk4YSCUCpjn1J7flR1ZNIV-i6TtblJr08DksLxtrGYhlrLW7QkD7zAOJr6msUGvBbPNlY-hgrLhQu9JPH485ZuXWlMY99RN0LsewhEx6uQ8wi2NY6e9tO6xgxRacHZ5vOpaFFKIIV0pSYU4mwiU9p_pUPCNdV6_TeZG4ChqTMCq-iezidL0UrbfW-WlDivuZNGxbWMZ52zblO4fhGFeXs4UE1ky8oslnt3-U6IOa2jL61GBobPyUoob-8zL0V2wOcbcgaxGLW5YFeiIlb9xJsowkGonMFz_y4vemfMn1_dUnWlsswZEU8sEJZdJiDRc_VcYk1l093fl6zs0rYjF40yiJrLLbycUu1-54bls2Y6_ouSqYLW1PrHKmdhWDhvuMtv5DDLgQJmo-jAvhsXob_MZnLOWatFrHotLPa5zTGaKcYmRk51ClgkejoaVtpSJ78GqXPTGwJ4G1x01EhIBnUKMyZr_K8Takjx2ONgvvatwbv15_YTGVdy_NR90s-ZwcG8WGVPywTH4vktmNwfh1_ScGiwRtqugfwiCnZhOfHtyYE3Fsr4EzasvBZLN0SgNnyom3T3hDi13obZEoAobjJQFkbUsprqZ8NnNB6Ga54adtxcLJzLshPy4y-uNdmJ9avuIfLUzwKO4HOSfCW3s7Z16mQXgj_8F4b7WbLUfuHC0FsMSRtzNElaGsyRC_FjXn9q2TZF5W39-7EB2nryKquszGHLXX6chXBOaJryNUeusqxSd26WRaf3zAqkhbOJpGP3GnV0opr9p8de9Na3jXIL9rGkD4ex32WTAmvCNptlhdEzlrNSi0yhf91KZddSIwpZpamCKPRTBGFTGedzLo0XRSx4jqCYB55JXHfbTymKBmIbxxsvsTQa3x_uIpwQXGqdmpUseZJuuz6LJRg2tRF-gZ0HEok8PTmsld6GgQUsyPBBsLnpSC_OpasiVgIYR0A5oRpJWoaAasfzlo6qmpxxssuEK7RJraVlkstZD2Xeirpmkpw0XpPI7ZQh7A8HUlXe0RhjvAXV0l2eQSBdRtTVqbDKPk5QLzdlkk9nGz5zwypVhj5Kwo99dIpVINLRtS2tO6iq61ugiRSOIFtsUCsvbw5XuvTJtVhF-_Gly9qmj1KUem_KmwYuH87vjGokE_uIAAVWsg_rhf7BGjN94tJ1WGxImxW1GFIILV6H2DdeY1OQ7S8OfSeb8vaKxB8iwuCGGkSECoWkHB1UyZIDX8BFX42jue5isQ1exsvC0-NuySasQHDHkQSMgcJsdEIPh_UUUVWjFVdOOUBVyAq3QDs88oeWSbfIfTu5dXupdRiWImb9VjmCbbV5iJp6TZcjTI1sE9E_YtZWa7c7pnEXdkOK5PwpgIzfGRToZGp_Tzwo5QZmwUUmJJcCklRIw9YB5wbUh7R0wvFn_lSlLYAe1F8wlb7YuC-CppKfr2lg25Lq6_x13vHjxfIjfKZDBGtK6KKDy7gUMQs9zKeayscFgf686EOH089oCPxYiXPn07McU1_n56SGINJOQEuElMHxO3kCKPX3IfMKQJiLKXhLjt6GUhy7vrBD2NfZjviw8B5V35heMUmt4sHvXOPYA3X3A2c5jqpcK-oxs-ouv-6ApiC2ZECfrp2tG6KqcQ-w1-HGvXHUkMF_pqNJKWu_YjXF6X59V1XsAoqScrTp_GiGySuqsYXLHfkq4oq1fKYLJ2H4Wth9k0wq8MQkqJ1llty-QzMWZLf5cZ6pVkWrJjnxkla9KFpvbY_AY9BsqKLaF28sY90TMOKzzyP9Y5yBe-QPhacFJzeQtYh1b6wKFRca4Q1eMot5uKrZxPmya6sS3VgfG-xvKrBUJ6jXKrn-JKc7pFEjitnZveUmQT1uyBuvRhysRmhmBXGoQzSg6mZQv-wh1ToYoG5HynRAyJzLXJ8QkE_kUHX73YZ-ELIJamnX-r18nMMeuWlc54zjyEiWkGETLOCS40oUZN8_HSYcO2cY-L5q2DVRUmWL0munnPpXBz8N2QjZWuY807Zp9V0Iwv5WbMCXDyfq0Xk79AocwH-B4BTuURCF8zrOHt5BtqVtLfFod7hZPiYv-NiuPrWLfK4ZfPITVwvAmUyO-VyViKChX351u2SpR3-sBnLnM6R2XOsAVvlsxk3qn0enKqT77elS6GqWCSs_gyuUEoViKfsVpZTEuMyD7vN2qjxjrRcprxyk1V4DxfsRl-BiL0sKr0Q-hP4_Up211mqNMutpgDL_sRki6LgJz8yWzW-r4cuae6lJ16eSiqwVwytagkaSW_Ozsq18PKU0VIVMavFvrMQZcnQTGSFHutIy-ckc3dDNgvOggj2Pf-uMj_SVy0U4qasyOrCwa1cxnkftFMQB8fNT9jSkUQsmLuBTu70nFejiTstiwj4U7XEIOupuK64gYzgacggrZPIFmvkJJ4iezU6O_AfI9gGRYSQ-XbQoDGci91ORXhppc71YX6ibNH2eHXs2aYwl5edoyyihLp_Hk60tCJRmx_cBuqNol-taFZptJi3S5BeLiz7coSFD97HNKoQ2g-jGejDJpn1Umv-6iA3gh0KRSlXkUsEbZwN9nenEjVnX0yhdT0qrHjZg1Pc7tFIlgn3UYy8qKRdYlRmoTdbojM3tDtXgN0UHjulQCzg3tBHAZE9Op8d8qdd5NLTiagLI7GtEyRTQEmmL1wtbtS6n0YR79vCgpRRFvIZDUC0ZYjEfXRPkr9OTyDuIzU7H89A1Hz2veHfjM0Jm6_B8Iu_LJNufajlCke2zJhEtLUj4BkBhNyopuF4V8MDzkek6oIuqw_Xymv99Q_PfUK2jksiMtprLslVmVCRT2yvQQP_JxdxFPdo1ChJVJDcBve8lDCLGxHccdrq-WJhG63ZtXA3aN7Z1Q6W8_P57WgOFFVgmVuSCAhD1cZxuahmN1-v2Iq4ljV97Z6ofH_pJNdmNt4CF1vRnNgxtAhBN2QdcGXnxXP-T2TnPHvJ3xvWrfBPWRFccwWL7HziBLS5KhQqm3bV756r8oXzwBeouvT0YS1LxRXOErgpcK44oHIW4rHFxbUfvlRqeRE3KSFgf9a4qG1HGE4-axbs-XOurtrTN2hUpuiNmSn3otf9sMeKCe9BwIrm90HWRpHfKpLFcvtEA9BgjVcu74qZ8YHAiY5-zVzlicMPnyrAIqOXdaTPI4Hh9SvDGpJ5WQYuq32f03VQZwprpxC7Xjr4KMyhFM0EMGGqFhagXmDeDWe5Trpk1rnj9smPIVAEoepwf1PnjlQo2ClVdiwjIgiy5Mv-U2LfbjtKx5a-8UomWMSHUSVMocN2TlxF6etJBEdHEQ3jhe6-DuJ07-exp3yd5neK3gLKQQQXoDlKO9VFsX9e19I_115k0cBzmEA5FTxjG00b1eAX4mydu5ORMTRN5BSdYsxdDBd8K188XpTlPGOrbXgdzke7oGTysquUnwaKr-9lqvtRoqEQAo6xSlAxgIvhrvQX6Ac-EEugxzvSKkBDsn5uGvoERdviZl-RvFQe3y1y3Dd7ybVNzPqaLVpgiONtfgTOx4pJeuvumyQqjmt3JMpuHLdYQ8ML8VcL8A_tkndtbSveqLHSfZqcJ3aMcHstA7f1X5zgCyyXRxpNrpzQ_qWLQi_FCZfd7c-jFpPOEm91rtQCQ-sMmotGxgUcxCkIdIG7xrdz_jKO25owzG9yBnv9be1IBlynL1bG72imBnoYGoLZBPn3FbZKJ5XKeZJHk_3BniuQFTEVGZw9volQC_3z0JW8YtwPbveJ933UoIuxOUv8MTmLFhlG2mUyrdyu_5OqmMo8WZSZyXvkLyb8UmGVMgh8SIlC95woiUagtG7Ox7VFGXdfaaA0g1fGH8AfK8ygIeaLC8uwrBUM9B6LGBNKRlh0TNw6tp_d3NsRNMeu3VfR5od0gG20U1Gm92cwhzz-9oobSmNwySqM34Bsv-xb5PfZOw-gR6YNNUrN6P3g5CrS_PuSodXO2DqsPRbKh6Vx-5LsEtItvxIVjN7YjwKnJ63DBDE158L_EGchfksqcOyvE4iY9ppup87pG6DZ2gWsqjZm46LFDsL4GNmGubN2mMKhkE_E5ucKHFcR0M4KnIMZdPCck3V02ISSVrx2FW4QCmsOLCtJBDY-3IThnRl8UOyTcJI1WbnJRWCGBwICHmfrGBYUwTSwuLVvBNYPZFlIursoChTl3J0xxj-bd-9ZhnQORAFfg7SmZjHIswFhpKAkrSYyv8raBMWpWOV81hlaQyETCHwv9EMlcwdNGe-NGx7R0AO4diQpn3ZLGjOGLxi0eRZ2aG4YsWi-K3uGt5PghCp-1UMfnnn9KoVf2yHEEo9lhyDlH0Qu3obHW4FWdJFUTcTUzZCcAAJbRyJTNEGQSPKQqWpT4BZd5L-9mIUv3Ir5fJZKaSz9ZoZq68dj0ocGnlf8rnOoRjhfxnjHwuLLL2xMbj5wgwR_EG1rU0eu6y2KWwY2nS4DKdMSjUAu01YKpW-0QcORP11OdqF4HOwy2TLCx-cJ4nklUUQVAMQy9FK8sLxdxBqaGAKXlEjrvExNC0ymXGPGj9qc_Eu6xeQ_9kgOZeW4hRzNPmqCjXl3hvHMe7aI3PBnq1orUTqMtdjTA23WhdTxu_k1gdG2P8epYRsfh8c4qIViT9L_v5ojJV59IHrY4VhZoraf2HbVBb8lWocFGYXGeCOpcw04JvRSSMNQCl5JHW53_Qy7V-HPlzy52ZeIefBqKnbAhN_s233mMBIKCqWobwRaEf__6zhYAO1N5m5xfiolJ4BLfBvRCnUO9NHsUAturVeMOXb_Yet9XYAKcJQfjfMWQaUOAYwMJuaIOjYlDH-Ur7EDpedW33wlCjBsiEUbT30EDkc90WOJupPvmXmNiH-pXxsFwCxkJ8gjc7QnGgCrRGxsikmj4Y5Gc3BYyNBLqWz8VwIeY86OlPpeIYzFfH_3puh0t2AQcfvdxtnLwFQpi6df-4z4ST612wi-l20EkCAD1U0WjPiM03yu6OzfeeuH8IZkYkkrFBzU4-UZDu7LIQVqQ1pDWz5InsYqXqbHphoWBMtg_uwX1OR75H6kl3L2yEFlYd2dmYBmJapuiDc1O0EI6x9uEG9U6-CpcD55t_JU0cQiD0hpPtAIiw8zzsxp6oAc-OyryPCNe-UONFqqEfnqWBowo6BR6jmM56TPn7ltSR-cf0fpGiQkNm3vI-FJSSJ3q4PR7-WpEAsv4bBiJqq7abnHTfYcCXHp1sqvrRNBUoQAd1tBZRu_-h0ndhp5YjHgkdXS2XsOO8ycUslNbjLQBtwEoM420rk9QBkV2Dne5if5DAgSnlEEN-2EsvMAfy2PzmUszAgpDgxYwX3YyT0ErKn-cXmdroej3xMRR4bBSJiUp22alO3lPOx38BTs_9ZY5vbrTj8_Q2bmt3J9r_-1ERCp6Hd1u-tGvrHFESgmKGJr-1SDErUCoJJYkTuh8mHPHcAFRIdg99ejPHeH1HlYLfNjKr4m0JvInXC-fFsmV-iExMiGkQ3gZF9ZMc7aAxsh0w2nUv_JU3Rogr0T9BVRQY2sfqixUUgcH20_8VxtsH4GIQ82rE16beTVkjdzI6PEemk3ukt35NCgNYmzGccSO9xwsZpAjotjZTKcGIFZqTpSyvbGLjxsv34xhFsqKbPN7GFsalpAuUxfyxSk9dk5t8UZmSZK__h4USIiW9V3JHD7_t6535eeLT6kRZT360qn1mONHaH6YHzAY8Q16RVTDfqKZQ7NuIkAYY2q5ReYOZuMHjTHm7pr17n42ScROuXCpWEAbyh1EalSZVxp12aXuYlAoh0WGbmyu1OtQ7fN4OgxpBfpNXw446ed9UHMxJYVKTnDv-M6tDi_acY0nCyko153pjIt7ixorfh17XkBzlL3RUL9cE5KvSBmQs2PHClCKl8X_ivUL2n-CjzLx5zwNk_M9pesLyZAXVJPcDE5rHMjwtqW3UU6nnwTT-MfjUyHGjFtSN5EGJBDQ2WvOZuPFqlhNsRGOI60m5TbAXQUmvuQGFwM7uc_mVlS5tDcp_5HCIknVzDUeuBQhlGySt02W4bjcvp0RgiOdVnFCCqHiHv7s-e606GwRZ-nyGnVxaozgtwRd2tXTNxr3hkmKGzs1S0BRlZhz74NOiz9nOVG6HWUhDhklFefFIb9hxD-0j3XIIvWeqnNXTZ89Qok-GoBeGmZsn29tEdVUBczLqPgCh7MRh2F8c_ArC32_V81MAcSBwBJkk1qLX0sAWkfnvAQUYJIXpbJBLYDlhKKwgwwCQ-AI_oP8KrpR5Tp3TBLfd8C0WKeIDqM3LSb5bA7R4cqgSoSlnuoagYGrCnx6sh0YGZknpVX1SmRZhtraolN5uXASRUVYNRV50EV_rLF3cnVzCypK8e2W7XIEcOeNEy2fvYWudG1ehra3yP1_oDbjuSA9qH4feYjUhhppRshVdzz3SgslSHXBSSFV790t7SqR4YJ28xALma4NMuQV1ygNHbBlvZF5hpNfYxOAGNyZjBEKS9mzhKlVyfUo5myf9A8sIwh0WkL5TBd3q3HzIYoomQkFxLi9qIrtkEc5NSTZc-9HaY60idDi4NspIoDCKweJG8aMiOmM2PukTka7jLbZoMaYd9Wz2sTx7XFG4czXfAxKhRkR-xXCbJi-uQNwap8dUuYg5n1CGrsmpGTUx_rvlFrPrMJPBLfVsBGvyvs2y91HpugpJCSKhoSfzQV4a0dmQ9qOP0AsdW5m06Ps6Sxh23Z2_ZcmxppDJ5ljcgipQ76YBDq_U1tokvAueanjMvwp8WfnAN7qPsAl7-EWnnPaA0tE6rpiKq8xet98aNTydxMRjn7GUxUlLC5t4fYBbtbwjOwsaNkCaFtTRU7ZwUuwt0kJzj388ZYNGXC50V75gLQJ0lkHO-qJxXev8jSCuBTC0DBQYql4cyTAmNBM5n6FKxzLFSfPnpK8t6E-HlvhlSDjA6Om0EIdrO8hBk0kVbrbxWAGon-UNXeZdbBvYIyTEq53eajphoc5ud4kA9YXHpLbVj5SRSFJCH1XFDGfD6FOcjWEY4Sb3p6TzoPhLKlmOJmSWOr3179BIQW1hoYCthEnjLRv4JZMzXmXYoX2GtFEu1zNrow08pkqFITKx_Uzl8BaK0G1PX5W-r6V8Qsa9PLvEhobod3aM1CqG2ivrj1sMLaFRy3cPeJefpQ5WUjJfsI0R3MlvYpUeA6v_G7HzqMQV-56G9VZmZzNnZ4_XJ7OgfbRP57o8KtFMg9hs6NwkzMkDsIrlyAvG6wFd3hmVakqtk_IJwe4XmG8q86AE7TIWw0TNZzZ2F1XkM44SI75fqaMt7LZ6OMzXrCQzSiWwoscz6RdjLAo0rXKScb8-IN4EczNbuOO0CpHPb01ICZoIkvcvA2jNbs-YyACDTlPbz_jHO3-n7TUuuOrCPc2N0sj246T_dDJMfIhlgyjthzitIhDX_uM1eaqHF0MrsLWsyhE5wvVRNXTON_K4pvukjnzN0HAhDzs6KdKxmaYCRZBmSPx7M_BquqgLSaaFNNddF9gk-726QVcNWEdhk2N0QbuJ4vwfm1gUp1dZzBw6Xu2uqJX69y7i8On6wW6QMucqYxlHI5ByxtltG6KLKSYea4G_74zMv7ryemyGTblCFvcDzfw6RMNb5appsNHX5PQDwVQLc67BFUZIO8dvAtpePWctHCosW0_mKJJOHTmI6Nh6mQTHBptFOXmTKEKadS3SPRQ8qtLhDSZjLimo0t1zRQLeRupB9_gNVI8rYRtHrp5AxrB6h1TqbWZ5PsWFQgfuQNQi91k8H0gcLSNLwAfy6qHhK54tISKnyPFc7slmuIe7_6vvM1ovI-kwjPoiC2-fMwIWRdjo8naQO332CNWPzCUMopMdjrEtcAuB4vaf5hCXvbhVCUkSPlZ3ZE6Bk9KfznZK75C1pzoudmVE0n3d3uEkxGvtL0GDcwBBDl_RtXzfCzY5sUrZi57H3caHpZoHrFj8raeEfkjcncX6wYrzrMv2CV6jAmpzdhIqCPh_V7JLBc6-8YXccJ9hX-IDOEwMNP58Byajfh1sbS-yTBGhbdzMKIxTUvWwUho63IyVdyIwuWUDv_zoOodRZNN9okUr_e8jQGuXWSNjAf4RYOan5aLsMZ-AC337HyV3xRZHR0l_e7YZvogiIVj6Yh8q8go4BgzzrA2lIytqTN5yZdABAfGla0J8ZYdkqE3IrAbYRXbXcjOGfxQifjyAu1Cv5Vy4EjUFotR3PR05su_9cCJFwxBKEe8BHvYtAsRz036__CsZdNkTs388kYZGARQY26YK3fgnH4lGKPZXlbAcGoUmnR7LmU5mUFSa_X1BNrQP2GeVkPIgIeRTtOFVu1R3sLZ3hHrc0WZXAEux7DP60kwryzPgkxffekM1LOsy6L0fi0L-hq3RIOIo06uowRUD-5g8NE7E8C6vHlzRGMOCVvM7HO9vOO4x1Sd9hqoZ-YrtrZ1hqqvz4ZZKQ3h-SgzCHoGdNhG6Xae4wCedtnhuWdYy_tJun3ZNMrVnCrViXrvVz-vQgYDGaObDlA_G8PE4x-DsdMMwWCTg6bQOUtp-0WAy35tSQbxFSkFEpIwJDXddwwp7lg9isuh4gG7vgidHwfKXaemDrDIOTRMB5YfT-AFDshDkmuGgs1oSeR2rRBpcHxwJUZi3hnOHqlp7pnQER6pBq2qwBd8RaoGSktaDKuCWFaigmUB0QfgzbmQQ9Bn-onRXcZYmxLbVJtVIwy3rZ64qhwuV2fp2wCdCvEzdfIdUlTRZl5CnEG5jIcOJOT5DI3gFVslNW2ilngE9Kqg-oq6Dpt2FwPsGiLMuU6Vw0_fNNiDOxrOWt4t2HVm83aGrPzCFnbSFVdAA_pMl_bZSoyqq_1iiAdRLdXCi90B3LMkuwPh3oXEeqZgFZahbaUqOQO-OVOG4e4Puo_wb125gDpitBNSxwkEATPlNmxf6D0SKQlzCp5ojhkn8SmMRqpwFltF4CXABZjkjI60ZPhA7QVtMjvYp3_RcocrwGmHdhK1BJ4GNDxq7sM__t30C4hvoWvOrzEM8XdTjvXCf4wWXbmQcsT-XEHh4WEm0xILisI5KeUMYiFIW3O9XYQknUwvWfQzvc-J1ZaGtCRaKqHPJ2PX7eDqh4p480aCOp5-yn00t11_ztvmNKNbAR9WMIgsDYe4qWEnzUxgd0JQ6QJFY-msMJT64jFwwEVqoylDX57y_A8atD-W53m_LNQquF0QIkF_e2nCtRHYUY9m68ENiZvIy2q2BwhT1rxG7tep_XsSVgJsduOeKrUZtB5tL4x7zVsRavFKFOUoi_rAK_lIyoYWPWp5uJytppBNU3yw3jdADxnrha3f5ZCY1a-3DK8DJJGsZy1-cjve7QPNaYMimN17l_GlJK9xrmxZdr5aW6vv3LNnn-DGK8oH89CGYUv8tZOtP8O0GKSNHOYoqmd8qVqrFXJ-p77eDW0OqcOgs98ELPAX6nxCb16ALQh7QMPTxXeqlesIrscHBtk60SHjnKBe_aJrLNQwJiMXdkeNT0ptU2uuWEJbTy4pinKEAt88_4-bwCWwrCcqsnQckyW1cVJRYXJnargyG5um2JfN91iGKkd-0mP1Z94KmsdU6_e7-11NEwsbp6_jnqkThRdiAoxEsG77oUZ3VIDKEHXXxd91W-p44DorrYhjvLXWVNZ6DsgI2nyeWRft_kogD620S3aT6R45j_6Czwrpo0r68DWcvma8zISzzSafPi0Qly3QPFLnanwqN-ixzaSXWp8uMOMxQ1-Lwmtpg2wWlg4pFIZU3hdW6KkhJAYGucdngAFKHyqdF7aG9zUIxjnWM7UC_Gwl09_dzN4NMjibAbSIAYNaACrlHWE_fIbjPti-qIEiDnPhFaBPlVPZV_56CVXb1nFKsn-Z_fwFRMAXuKDSFV4f0M5Lak1VzWOz93bVlS45WcbXT013NEes_oEy67kxbTrt8ILP6BAVWzxRFL33L23Ap_FqnoyDvk4jgP5j1DzBwA8NIp_R1ldAfeuUWCiH4er2QTQHusiqL2d1jTD2JEfEHbmZMVJkE6XTCZ_JZY1F15thX-Nd7V-l05p18Rb3f9XEIKAsPqQYiiUEamvH5Wo4a2TKVFiXJXVXr5h_vjQsQD2Il4YDLDD61ZyBWbiHXLivWlcUVWcb6ZDzLeqB2NftFvG2PvMJ6edBdcUa_-i0FVInGAsMQeNIgZIAszG_WFrJ4bKtSdZkXDQm6Dr4y7-lpUNKO06imFAX3ibR5pU0_8RZh5T22FkcwJnZreJcXMNRHl51HIAjtYNobjP5OE2UAG9apyJD_hy2Pz93ZeEgnB3ftY5BV2rtE47Ly57OxV5dikvg2lBSL-_2WnC_KKj1SCQuPEJry1V731uwRWoK3Fo_x6N2yyBdx_FmjNF2PXG5x7OmMbgyz3KYzyiSUvkWniVTf2jso-7vRcFXV6MGMha7HrY-OAMHkq6YiOv2rBqhKuxgg1bFTwswoPDxDaqUW8rbfkWg7Zw2cQrUG3rU8ieOHxUXgPPtDyEfU2RG-wmKIrW_1nWmD_BmpSjdumNM8irf8ola5IuwImB2HF3eB0un_POuKvEcZxv3h_GY-xhVuiyeJGJCRZUXAk2YkgpIkm32zUqumn7lG5RWbaby7GQBkS6WJtVmAZRT2CxVx6yETHgQbhD7720H5ayyaKrZKKRH3JA6-AIZpIEeYVe-PgLKuO_r_1yn_1RJGzrVqdT0tuI5u3gv3TZEZUBbk7gQtCZgTQRSz5mzmEp4qKrrhSsGEzzYAjvmtqVhUYoyBBfirf0KMWgzysecOIOxjnE49Ht4TIYooneD9_CNSDIm7CsvodLaHz4bga8jWT8Giw9-i4xXOgBs2M25mw_WVlPzXc2Nx0jglfaBYPCTLr5jZ-xDUvCZ2hUnsH9hfdImAUnzViVy0Eb975_hV-hqwc5Utadz_oKvNCdsJrLvlnR3lCNfxeVRX5cz-2Jm4yJ1hQ-10JMz_HgRoKBS_xkJh8cy_DgdGJYSIi6WJ0K7sNOgY7z_vVJDQhV-gsTDTFXxUAgUDgEEIrX4iTp6vzbSdWQgr6ukxMY7eY6vL-R2yv0QXo5iCnU7jPWeQtYXQl431miabCyr85mHuwCN2hwCWT9omDMvP3sqp0hw6hGZXPPZUlcJXF4qgGQlDJdbX0XcP5q1QWzJ0UBVCT7H1-3pg9Qt9p-AnV2SDN7JLc4aY1ACOwAD9z44KXJDR-qsEhCvRoyUu02IvlzCh73zhKNobFuWqg9xDZfRCKuWSRBf1G-FC2IFb40z1AT57vfZ4yV3dKJF43EBXxWBtAak5ClYuOxwsaPEx7KIGlENGyCvFPtiaL791sVU0VaIZqpJa0CQrTig4yspgn9OI-fOO8hDxo8657ThDJMMJo7yz-FW2B95ca0Tw_b412Q0R1W3dezfQxEqQdlX_JY9TtrVjuu01fM34xA-2pVdKVFz1JDcGQn7cK4meuSFWrnsv5yAQvg1aElqpPz-_bwuKne_DmISzFqNHSApD0Bp4xf4RRC8AoewVMbPLOjtcLRLERzpuoE7tikIzfXycgViCD1a8bmE30IVWqCHIToFixpoHtxsyiI5abZsaGhqgpXMQ_XkYWM18yKKejNjsmLdeQN_CF5ogieljSqE5grIx_7sOXN5NXGyxntfjwNd760fhmQbY2A-z2rDz7Qrl-wgZlDgIDTsiOUQLqyzyiDQT3Mo5HS2jNFaBeLnKLXPKO4JYnQM5jW6I019smSLlZ2EUrPucNwc6tsy7ZIfKJlfBFcu2Z1iXTM5qwX1deFMs6nhF4ma_iEG6n-JIJipR-dqwoj-DJ0Wt9cwiQDIKqPOnE3-n50J93_7BMMzuczrd4ze9T8HeNXrNMBdYPYRcNzwsKgbx_V0Py1QYdC0P1bJDmt08-rjQ4wwhei0X41-qssEFgaexBCgZTp0F7nPtoJRN4UROHQRs4_e_b-9c2z98wcbdmYXmisfgeuRCUZQlTzK_c32MVQ42aSzJr76Z7_s6DA6eJlAHkcLG-HPPNx8a4n4MXa8OQpTo96jTzesXI7XRWBJEzvoxQtbc1I4hSqdi-w_UjqB9PR5FfrSyZZ65HcAQlE8BQF-ZGyxhyzr-19Umro7qiucTUtiN7vI_nJzLh-Ra87KHMi8zdDiD1ip4fkDcS_dC0HG1ESz1R20VtDSeGbc8vQ9sbsq9pqYNOBZo-ce0wwPlhCGyFO3GGqT0hMeJWIIK_TjxktrxNgQum5zE60-9PSl0uLpAQFDe4GxjXBgyU8vZaAsJR49cBO45rrlYuD4S1y7mCizcYZHJnF_Elrl9cgcZPjH5V7tB9Ms0_5yoAW3AXN_IAnBl26pgjaifV1fuwBTcVrYvmNV0BKMrz3f_BPTSXFR_oC_Aug4DrHudZXxzY-8C8I6VtjjqcmKTjacEGpYKZ6U_f5bNxt9jymfLGssOhKumjyuPl0ICAQeJkltKdOhuyOYxucF5iElkTKVWqZPgpfJHO9VXfRrECjSenSB08uUhGMY88gLla0cB0ck8nxMdGBJEmlSAtRSUpdPG_E-JAF0UcRrbP9lM7ePaNFWtV3yiM75plD-jdD0taa8G9HIedtybr1R075yvfJ4QMumN6obLb1LsiunMQ7xJdHgg7CdMIcTfo3xmkiPyaQ74v8oQ5FR_jvL_qMIOhcppkXLbZJwbkGiDPdJYE224_xG2WT0p4ha1zP3dcpQQBr-PoUFO2k6dQ79sAIUAcYVc0g_yTTOFRmXRdEzAE__V5qXua50EW_1mJfmL5EbL9XEO2aaqlrp9Csu1uM3sN-vCX0esr1pgtBr6z6hV3YXtDWSWc6oG68Jt6T4ekMVO1KBoN_uyOLfXstPFuER2ZFSzfoIatO5GRbH9JE2m_6np_qnrXgcL0bkPqy928-NeDkv9Qi5EW2UFAdQgN_XTB3ozv8JunH6lBpo9tLb9t8TmBlhedqjZO3GcsEL5jL7GqmCCYsw9ILlmz3tissKXwvVUaIxhNO-2FH7Ef7yICulAQQxJJPfyHqHDUr76NxeTwqForrt2qm3IbHMrzc8MQuaWilAoldEk2qXGCqFEXNUa8h9W5shbGZqLOi1khf8TJMOS2xPY3LKaLdrSksLjmPObNfDwkvDhCbFWGWt_FpfHK156Hulf71crBJUL1OFJo-7XjDgtf-sYn67QwxSiAbpmz_5yDpHTOemXqsrdLGL3CV0ZYresFEH8NNHJlJt72O6Y15UUCAOXIeoS2U1O9s1OZ3M_MXxeA5cyq4Idozjjzgwxdn4vZ41gF21X89Ur94aMhV5LLv0PhO9EcP-u8V8RxN6Bio8ITjf4ybkbiBtx4SyzI07uM_pZt92lhZ0i6Fbu50NPqNmvWG99ETpzfDvPnjX4OHKMAa0CLOLWi-9fE2GVK91t-UnTB0EffvGe7f4sbnvUkp2CZmGunGlsa5FD2QSi1DN2dU6oHdwiTzIBEj_eC6BdXalGBcenofcKCUgwaTF9-Ib-_rD_Uj7AK6tqe_jtCEFOgTOqoGgiWyYu1rhSK7pFdK-jajdt5LVbWUqKvNbqI2tJzYqoe-ufJT4JCt2ZFPuyUHvdKg8l7IhWr4bHe-7jNoG_Cd_pZ-v7u1prExWwRJbEC12bHiSVPvyb3YxygMhdk6Ntw335aY_wLVT9CtNL5jquwLtCfCJQZ_1UB_peWiYqHHziufi6x52hihXBVTUU_g86JfTPdQRXIAzZ-l97kchJMD-uSvKbWMrNlSASsrBKIePpbB6IPABm1IUflr3jLMgJvygGp-CIDViK8fVtiAQHJdTiFRcPqWlXpXccPMgwT3kZKbioucmH7-mkBc_9DeJCMRRiPtPtnwn-9UvPENpHOKf90MJIyVHi2WbDCK2qwcOKm2dxHpwizGbLnqWV4i-XL1kxsCXh5XuT1wrCCu0yh5XdObi2i6olqGYqbiRJbwHfoZCyOOGb825b1lgfeoTp9esrJqFRDXV19m2it0go8JGD3Qc64rE0RrKcKoabrmq74Eh9WygxJDe-K7lISMl3Nah4bcEmHoZ-hEwdFD-kzn_6dm8dRkT1ZEC7un8Bcp_Mb8g1uXcMJK040B7EF_v9hvRLsASaa3jZ-j6iodmzq1-r1yHzsqdtL9XEF55W38MePB-fMoEvkEsm-kaIf1efO1t7z3NkdBEcgsu_ZQqBX9vwzY9Qe6S5pdJbe4eZcsBIjsgJiUGbzOc3mYQ8PFj5uDj9VuElO93glbR2dcYbXnXPzpxGAZ2QvkF133bDsKCatF-dROVRNTOc9fobTy11kmVOAWhsC1WWnvrmYCIYlhMKtKODRxxdwALJf_ldUbhCmDm4H8cvkfwsyX_gacfR_KHNDpljsbD6whqkDAe9DELqsPFPWPw54G8BsbIsZuXAVLBz7dr2YEohIK6T8mPEIu7iFFM2l1a3WdOrvjtsT3D3Iqu5XakVzHxiPOdvkrlJ1P8G9s7wl-O_NsgICPLdjBwA71xgGB6RkX38JnMq06h0qletVaGj-KZxT1vKzuZgLpf26YlR3wUrJYV_jDJbZsZ8GbHAoWubGxRWKfAnl7lBCCTA0r1ckLbxluVkEqWKUOA64P7_sj3Y15zuclWtBcQ4cTDQl_sLw-NYaArA0XHXuvRjnDc2YO_j8v4y4yrdvYVJ0R1Mkv5jNEhTjXSLfIgU6AMS6bpJdhIjlD1zkP7Jsk51EV4dMy082zJKKUs30XNBMfRF3POngd4_zPRXA8rPlS3Fguy33Ta4D1JVKGaxRGox-0La7a3t0mzu2c5yibKzLP2WqZUey6k-b0y-MqnEQRU-75qrLneMo5HdAJBCPuO_hTOy1dILfe5pYBXBUiW9gzrN3Uh6T5WHGWNS7DqF84O3WhaQU3VYOFmR1ofU3zg_zXZeWSmx3eU0rvZJBoULRZH2qjDWyjOXdLAj7oGfpcbuJX_NtnhVmNbCOM-ds87MjMjVE4lAxKDH5X4qVy9BUQy6o7NKs-ke9CA9-0dJHUEvjjP84jh0-IzceyT2TLjWJwxRMZj9M53Bc5hQQkNk91T-yc5TGVIeiqZIL19ib8N0K85iRzS-xU1NPa8jE3yWyMxGkGPYoad8RIwBN5j0w1arSoBxpBApweTIh4KJQYcENoaMIssbHwIURO07-iDHlYq-dVcz1q1vpqW3argw5JPbBLoNwbTJeFtY8Ug0XOzE8Zm1EbtrM38JHsihIKaNU3OoGx2agcrdf6ONW0_aHRdgRvOwxwfeKjgnFnGUCG0y_ODPgdPYpE_2m5zufo_NpswwfUozByA6HE-r_g5wtphb8se4tRHvS7k8bhRCrra3tl8vTl9BJDwX2K3XiZhyTHRwR1P2yRXUnNbJIhg2gTr3YvWCPxiC-87s4Du7j4t4-jHwPeGGd2Je5avCFM1QCKQC21IQYVN60vag5JiPDegavoMMK2kMa0kzEZvzQV3u13r_wXRbUf9B1Kc-gcqwVh6idFK2lfGHAPam00YehE-8aneLI2KkX_ZH53DvUbnENsieBWOZ3mKbKnhC31JuTPwxz8-aTH3r09qbRbjMXQEizhIod93pDNJOITU26Q-zsTPFb4TadfKLXVdC9S2vaG9rKxmm-TyB5wtjIpXNDh9VpkYxYeqNBRWz37PSCuswRlZjSOJ5em9PQNYowvzfcxtF5i9Luv9th70z1fw_jpKlr3Co9YJjONMeWL5MVPyyyRNraGJk3oFbaCZUcGo6YCk-nPbc7KVC6ipel6xsjRnSaOUNBCoMCKKlicL0I0zEaZRZ1FMuFbzLUOaUlZ6Gs5VYNR41UBcy9WTlG-EvGsxN6_ShvgwH86zQ1MpOMOn0PXUBah7397AzThrkQiCrYijaCpde4qa7gMvl0G7mFm1n5g_0q10VLn1lOSzzQdBK5GwTMymU7_XmOFYh4-hfERBSEhYi7Cx3kcaX5BaVqFU2VVwoxe6stPfeAJk_O5fx433UQsiRK2f0PveSSTAbXSB2vqXVogDhWB0-aBANp2zXN4SAI7JHVEF2nntL57N4KvwxogaqOU9br5f72WD7nLjnvZ9W7to2jXWo3eIcuqrw50LrGSjz-6cwkL5_J5ubGk5Dt9w8gkaaqYdmQwGcYl-P613ftWulCPq0BM9s58q02tXyQOxCuuCWv04y3VK5vRnyZiG-Cwc4PkVChzU26ejDTBy5dShdFkWqGTMZ_vKAUjnXXg_vkuo1yOHCOULqhmOqBOX7dQ2FCmqADVxIOhegWvg9nIIxy6SoM0XDrsy0Nrg9LM1A-ugCbVutK3xnEKfxiotrKKI_76owujtVJFsKSyqipdTvNV8gi2-OZiqPRFGrisisjqGY0Mi22ghtK0O0I9xzvNM-IN1WDDcYy2T2gP3iPdlYaJhl7MxyuJv4HumvhO9BIAxhlDjsHzjeGR1KS1aVBHmgLjq-BYzAsaHyk5iDyZ1Qiyn6hhcHlJbhjK2NkSRHT50uCN4w20CMVz2tl6WIRw2J4eMoaEPARUBqnuT71U7eoi90EYw2x8ToADkRgslZCO8HUC5trsVE-AoQoXZjf8Ob0xNyRi2KHLWRbeiKwdQ-hsWDlTVGdhHVVFgQ5iAI1vYIC8p9CVYkYqnMS1dLiIIogl1v8B1lZ-K0sDlSuJ0AOl2w9fI_z-YnAKWPUua-6WhAXLvEbKIemrzC6qjuoDt75ULNy-ZEp1UtdW7x4n6OZjv0Ua0BA-IrtdHjKiD_UykItCwxw54B3AR-WF07pQ3DpJQQEjJ3zns9ikFt4_-NwlAIpH_GpOWDCW6jLs7znvGODvqfhN1-9Nxk2VwWIaaV7YU2TxUEAiMhErxMBhsEKOBSCf9xp8clKXtoALbMj4Z9PE2_SQIdJ2acIVEqDuAXqBrtw7_ci3_mzGiDTtvZ_oNYbILyVwheBW0y_Fr8nyrhqCvmQQes2TXx6NML6gP7WKdGOwnXUzrP1cTKukcTcFPMMfeN2ih-elmu-YehbqUASMNJF8ba9rruUx0nsVrDQrz3vbeeTwGSCYDX68HL266cmU6O4JDaH24agvIbEQ1T-A-3PsYANYC_4WSlwYGKq64aOA4YYcldIwE_OBI3mBp6YpzogWYOOoMUUPIHEiJpr_5ofyeZlOy2gHWBlLIjLYsmG-vuRfZmt-5QLUCsXQ8eBDO-lP_bYPcG7Zu-yuTZUCwB8S_7ziRPNgtOoR2M108A_MYiNF7jAhel5qgiMXKJi_xEXPj4xDYAscio7MD0pCTcvs5duO1DVVpDjdgP_qaDkOmqBsDWP-cdAPZFLRCiXtO38JOXKRF_b_ww2W-RB1EvNqB5JaI_XyAQysCXAx16mcI-_mtbt-DKKcz7lIJuXFIM3k8HQ9Hx4GjeLLRgYg0H6d-l7figR_N0pVB2zcaVPecfdO3tkfG9QWnuS_WkG3ezEr0pc1Lrc8yiafQup0C6oaJjEknYHN3BvGrP25CCIgBjLV8LZ0mZVlc07cizX1LuQ0XSk4kcs_dQ6CWni_ZkIz6asgbHXnK1Tl4vgLZSOCKQSmGh6WVpmagN_kmOIeyjxnrR36gSNKu3mkssLCPUS9fE7wK7x3beVN1AsL9b11UGjMwHkkudgyp-5l0bVmhllCU61PPkFpq5UwI4OAipOAuX0DLzVxOF7IRwS6O0Fy3LNANF5PwBNcJBSK33WdxspelsMyhtL-KnXx5sWmGSQYR7St0VG32dix8EYjt00v4h8GbWYjH8fEzO5C7F5Rnt_4r8TcSx4c4s1y5-cxYSNR9kcXZ5hucR8EVNvU9CQiv6Qc3IYd8-r6mslOLNqoI5zul-OJpZHDZbRLyXiS990Yg8MegNTJuDXS73c-YHBW8hS-n6q9yPa48kejjhM4z2irPUBH1TDzUJCjYwdm9bK4bQsPIlqeW8uhikT18bgujrw-3hjXmP_n9KGEn5xNvTthxjuAD9_YfWkOo9GQNyEK-RTrCmn8aliF3WHpLoUhkRKmCvDNWLKP9NUxz8nHiTmcPI7Qi6U3bU3Dnr2FPZVKP3LqCqLAqKgCBoAU3kR_RtNLwm7LUjmT4Ck8uuBHSiNeTRsROYwNpUb95NHZh20Zj0sRza3sJxMubxldzczW3N7V-iPwb8r9CHNSPzlsUfE6G1ITZ5_GLVMqcAOwHDMW7_LAi2DnM-nCrw0Ku0qIHizTLbBUb_qNZ7uqaoDNqHxNlw2OmLQdktPtOkK5u2hpOy-x1c4YzDwha6kayNyg8iZhNxNRnf3LoMljjGSSLNb4SZ3fLWyGbfO9k4NIzSsqd0o6PS_FUUzl7vrhgO_IsURcHTGPZ-B-LpeEM_Qm3xXokgBHNec0Wg5lLceRPpRvKY6DdNg8T2MXIqU9FqV03C393wsVzpLx6drPW7pMT6rhy-BiFNnNPsOFQfDGAmCSO1DWoqZHJPD1XaeKrM_6uzdnvVohFbkeDv4gyttR1xA25jfF4LWZTEcYjH7mZ0TnIzfWGGOVG0xoWeMdPWoaF4DD_ropnRLkq0fw5DayOGXxfXOPPvpO_T4tR9MauTMC5Gb083sM6v4O66mZ4evHYMjDOr4A11NDzFYimfrRXwpZC_13JqunGDcS6N_GD5pLTKkrJyMS_gfFun6XvBmwAid7qFyamTM8Xb_l9JVaKXms_ScLkF8OvaSQXAw_LhzIdWiBIoi_FoJjPCmjcygv9sYuoGgkHNXxVD2rfVqVrRxuRH6JeVGt-v7Ug40N0YrQjkNLMaQ2nz5FMG-6hMBh7sb9bsNIP1evPmdEMQGjt72cnh_LzxjFbRPakf-l-kXkUlFo6LhDJuWVBB4SESOBosykzhHKqlaUmvNg8AXc8QoFfqwicWdwW4lqdXejXJ-9iFwCJOyJRXJ0603ivVg0sY1waVF0G1YpqX6CDW72hvt7SpVcdDds-f7C-uCb4Y6EUpRttzv8Ho6vcfhPUJsIUVE0Cb1uWOZycD_wqoE7sFpS2RAlSYJkP7xDZGR8sCSfhNQp43AAZE8ZMyYg09yprcGQzFl0N3T4tv_7vu2iUZLKsmdw9zgwwl1TpqjlQbAfsC_eiLElOPcPHD94ybXQqEhRuSdX4KFmk24ZHKht_dSY8jF7LsEuQBzfy-eOD1Rb9Gz9ajFnu3Zx0EBoHseXAunMKeVTnbbeVA6RIsb5AkFBVMUTfylvYVMZauK_ndYCO8mq3rGd8n9TFoGHgGs5TiCT2MWHkIrKdXKIKEuJEWnKTLzsR7VD4f8Xey1domxcuTRgWqCGHtir-l8qWAsYW-DihoiqdV9UEROiJg-J7qlx29hAKLYtOHVCjyphit3zMxyPpDi5psaNkCdTR1Ils5deFGE2qsC_dU7D_74sEkdTRiBadE0_am3cXY8xY2QRlz_qtZiZp4-SQ5cxuGTRLsK7wwXd5tzX-urc4t9dYiofLSAJKXtpGSISPHk4TQYrrGnTE0BzB1UOnQ== \ No newline at end of file diff --git a/main.py b/main.py index b096156..573e7e8 100644 --- a/main.py +++ b/main.py @@ -2,12 +2,14 @@ """ Trimble Geodesy Tool - Hauptprogramm mit GUI Geodätische Vermessungsarbeiten mit JXL-Dateien -Überarbeitet: Export-Dialog, Georeferenzierung, TreeView +Version 3.0 - Überarbeitet: Korrekte Netzausgleichung, Berechnungsprotokoll, Datenfluss """ import sys import os from pathlib import Path +from itertools import combinations +import math # Module-Pfad hinzufügen sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) @@ -33,6 +35,43 @@ from modules.network_adjustment import NetworkAdjustment from modules.reference_point_adjuster import ReferencePointAdjuster, TransformationResult +# ============================================================================= +# Globaler Speicher für ausgeglichene Punkte +# ============================================================================= +class AdjustedPointsStore: + """Globaler Speicher für ausgeglichene Koordinaten""" + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance.points = {} # {name: (x, y, z)} + cls._instance.available = False + return cls._instance + + def set_points(self, points_dict): + """Speichert ausgeglichene Punkte""" + self.points = points_dict.copy() + self.available = True + + def get_points(self): + """Gibt ausgeglichene Punkte zurück""" + return self.points.copy() + + def clear(self): + """Löscht gespeicherte Punkte""" + self.points = {} + self.available = False + + def is_available(self): + """Prüft ob ausgeglichene Punkte verfügbar sind""" + return self.available and len(self.points) > 0 + + +# Globale Instanz +adjusted_points_store = AdjustedPointsStore() + + # ============================================================================= # Export-Dialog (wiederverwendbar für alle Module) # ============================================================================= @@ -203,6 +242,55 @@ def export_points_with_dialog(parent, points, default_name="punkte"): return show_export_dialog_and_save(parent, temp_gen, default_name) +def export_text_with_dialog(parent, text, default_name="protokoll"): + """Exportiert Text als TXT oder PDF""" + file_path, selected_filter = QFileDialog.getSaveFileName( + parent, "Protokoll speichern", f"{default_name}.txt", + "Text Files (*.txt);;PDF Files (*.pdf)" + ) + + if not file_path: + return None + + try: + if file_path.endswith('.pdf'): + # PDF-Export (einfach) + try: + from reportlab.lib.pagesizes import A4 + from reportlab.pdfgen import canvas + from reportlab.lib.units import mm + + c = canvas.Canvas(file_path, pagesize=A4) + width, height = A4 + + y = height - 30*mm + for line in text.split('\n'): + if y < 30*mm: + c.showPage() + y = height - 30*mm + c.setFont("Courier", 8) + c.drawString(20*mm, y, line[:100]) # Max 100 chars pro Zeile + y -= 10 + + c.save() + except ImportError: + # Fallback: Als TXT speichern + file_path = file_path.replace('.pdf', '.txt') + with open(file_path, 'w', encoding='utf-8') as f: + f.write(text) + QMessageBox.warning(parent, "Hinweis", + "PDF-Export nicht verfügbar (reportlab fehlt). Als TXT gespeichert.") + else: + with open(file_path, 'w', encoding='utf-8') as f: + f.write(text) + + 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 + + # ============================================================================= # JXL-Analyse Tab # ============================================================================= @@ -255,29 +343,36 @@ class JXLAnalysisTab(QWidget): self.stations_tree = QTreeWidget() self.stations_tree.setHeaderLabels([ - "Station/Messung", "Typ", "Prismenkonstante [mm]", "Qualität", "Aktiv" + "Station/Messung", "Hz [gon]", "V [gon]", "Distanz [m]", "PK [mm]", "Typ" ]) - self.stations_tree.setColumnCount(5) + self.stations_tree.setColumnCount(6) self.stations_tree.setSelectionMode(QAbstractItemView.SingleSelection) - self.stations_tree.setMinimumHeight(200) + self.stations_tree.setMinimumHeight(250) # Spaltenbreiten - self.stations_tree.setColumnWidth(0, 250) - self.stations_tree.setColumnWidth(1, 120) - self.stations_tree.setColumnWidth(2, 140) + self.stations_tree.setColumnWidth(0, 180) + self.stations_tree.setColumnWidth(1, 110) + self.stations_tree.setColumnWidth(2, 110) self.stations_tree.setColumnWidth(3, 100) - self.stations_tree.setColumnWidth(4, 60) + self.stations_tree.setColumnWidth(4, 80) + self.stations_tree.setColumnWidth(5, 120) stations_layout.addWidget(self.stations_tree) - # Info-Label - 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) + # Buttons für Protokoll + protocol_layout = QHBoxLayout() + + show_protocol_btn = QPushButton("📋 Berechnungsprotokoll anzeigen") + show_protocol_btn.clicked.connect(self.show_calculation_protocol) + show_protocol_btn.setStyleSheet("background-color: #FF9800; color: white;") + protocol_layout.addWidget(show_protocol_btn) + + export_protocol_btn = QPushButton("💾 Protokoll exportieren") + export_protocol_btn.clicked.connect(self.export_calculation_protocol) + protocol_layout.addWidget(export_protocol_btn) + + protocol_layout.addStretch() + stations_layout.addLayout(protocol_layout) splitter.addWidget(stations_group) @@ -305,7 +400,7 @@ class JXLAnalysisTab(QWidget): splitter.addWidget(points_group) # Splitter-Größen - splitter.setSizes([120, 300, 250]) + splitter.setSizes([120, 350, 200]) layout.addWidget(splitter) def browse_file(self): @@ -323,6 +418,7 @@ class JXLAnalysisTab(QWidget): parser = JXLParser() if parser.parse(file_path): self.main_window.parser = parser + adjusted_points_store.clear() # Ausgeglichene Punkte zurücksetzen self.update_display() self.main_window.statusBar().showMessage(f"JXL-Datei geladen: {file_path}") else: @@ -354,130 +450,96 @@ class JXLAnalysisTab(QWidget): parser = self.main_window.parser - for station_id, station in parser.stations.items(): + # Sortiere Stationen nach Zeitstempel + sorted_stations = sorted(parser.stations.items(), key=lambda x: x[1].timestamp) + + for station_id, station in sorted_stations: # Station als Hauptknoten station_item = QTreeWidgetItem() - station_item.setText(0, f"📍 {station.name}") - station_item.setText(1, station.station_type) + + # Stationskoordinaten + coord_str = "" + if station.east is not None: + coord_str = f" (E={station.east:.2f}, N={station.north:.2f})" + + station_item.setText(0, f"📍 {station.name}{coord_str}") station_item.setData(0, Qt.UserRole, station_id) + # Stationstyp + station_item.setText(5, station.station_type) + # Hintergrundfarbe if station.station_type == "ReflineStationSetup": - station_item.setBackground(0, QBrush(QColor(200, 230, 200))) + for i in range(6): + station_item.setBackground(i, QBrush(QColor(200, 230, 200))) elif station.station_type == "StandardResection": - station_item.setBackground(0, QBrush(QColor(200, 200, 230))) + for i in range(6): + station_item.setBackground(i, QBrush(QColor(200, 200, 230))) + else: + for i in range(6): + station_item.setBackground(i, QBrush(QColor(230, 230, 200))) self.stations_tree.addTopLevelItem(station_item) - # Messungen - measurements = parser.get_measurements_from_station(station_id) + # Orientierung/Backbearing + for bb_id, bb in parser.backbearings.items(): + if bb.station_record_id == station_id: + ori_item = QTreeWidgetItem() + ori_item.setText(0, f" 🧭 Orientierung → {bb.backsight}") + if bb.face1_hz is not None: + ori_item.setText(1, f"{bb.face1_hz:.6f}") + if bb.orientation_correction is not None: + ori_item.setText(5, f"Korr: {bb.orientation_correction:.6f}") + ori_item.setForeground(0, QBrush(QColor(100, 100, 100))) + station_item.addChild(ori_item) - # Qualitätsberechnung für freie Stationierung - control_point_residuals = {} - if station.station_type == "StandardResection": - control_point_residuals = self.calculate_control_point_quality( - station_id, station, measurements) + # Detaillierte Messungen + measurements = parser.get_detailed_measurements_from_station(station_id) - for meas in measurements: - meas_item = QTreeWidgetItem() - meas_item.setText(0, f" ↳ {meas.name}") + # Zuerst Anschlussmessungen + backsight_meas = [m for m in measurements if m.classification == 'BackSight' and not m.deleted] + if backsight_meas: + bs_header = QTreeWidgetItem() + bs_header.setText(0, " Anschlussmessungen:") + bs_header.setForeground(0, QBrush(QColor(0, 100, 0))) + station_item.addChild(bs_header) - # Typ - if meas.classification == "BackSight": - meas_type = "Passpunkt" - else: - meas_type = "Messung" - meas_item.setText(1, meas_type) + for m in backsight_meas: + meas_item = QTreeWidgetItem() + meas_item.setText(0, f" ↳ {m.point_name}") + if m.horizontal_circle is not None: + meas_item.setText(1, f"{m.horizontal_circle:.6f}") + if m.vertical_circle is not None: + meas_item.setText(2, f"{m.vertical_circle:.6f}") + if m.edm_distance is not None: + meas_item.setText(3, f"{m.edm_distance:.4f}") + meas_item.setText(4, f"{m.prism_constant*1000:.1f}") + meas_item.setText(5, "Passpunkt") + meas_item.setForeground(5, QBrush(QColor(0, 100, 0))) + station_item.addChild(meas_item) + + # Dann normale Messungen + normal_meas = [m for m in measurements if m.classification != 'BackSight' and not m.deleted] + if normal_meas: + norm_header = QTreeWidgetItem() + norm_header.setText(0, f" Messungen ({len(normal_meas)}):") + station_item.addChild(norm_header) - # 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) - prism_spin.setSuffix(" mm") - prism_spin.setProperty("target_id", meas.target_id) - prism_spin.valueChanged.connect( - lambda val, tid=meas.target_id: self.on_prism_changed(tid, val)) - - self.prism_spin_widgets[meas.record_id] = prism_spin - self.stations_tree.setItemWidget(meas_item, 2, prism_spin) - - # Bei freier Stationierung: Qualität und Checkbox - if station.station_type == "StandardResection" and meas.classification == "BackSight": - # 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'] - - quality_label = QLabel(f"{quality_value:.1f} mm") - 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 - checkbox = QCheckBox() - checkbox.setChecked(not meas.deleted) - checkbox.setProperty("station_id", station_id) - checkbox.setProperty("point_name", meas.name) - checkbox.stateChanged.connect( - lambda state, sid=station_id, pn=meas.name: - self.on_control_point_toggled(sid, pn, state)) - - self.control_point_checkboxes[(station_id, meas.name)] = checkbox - self.stations_tree.setItemWidget(meas_item, 4, checkbox) - - station_item.addChild(meas_item) + for m in normal_meas: + meas_item = QTreeWidgetItem() + meas_item.setText(0, f" ↳ {m.point_name}") + if m.horizontal_circle is not None: + meas_item.setText(1, f"{m.horizontal_circle:.6f}") + if m.vertical_circle is not None: + meas_item.setText(2, f"{m.vertical_circle:.6f}") + if m.edm_distance is not None: + meas_item.setText(3, f"{m.edm_distance:.4f}") + meas_item.setText(4, f"{m.prism_constant*1000:.1f}") + meas_item.setText(5, m.prism_type[:15] if m.prism_type else "") + station_item.addChild(meas_item) station_item.setExpanded(True) - def calculate_control_point_quality(self, station_id, station, measurements): - """Berechnet Qualität der Passpunkte""" - parser = self.main_window.parser - control_points = [] - - for meas in measurements: - if meas.classification == "BackSight" and not meas.deleted: - 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: - dx = target_point.east - station.east - dy = target_point.north - station.north - calc_dist = (dx**2 + dy**2)**0.5 - residual = abs(meas.edm_distance - calc_dist) * 1000 - control_points.append((meas.name, residual)) - - 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)} - - return result - - def on_prism_changed(self, target_id, new_value_mm): - 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) - self.main_window.statusBar().showMessage( - f"Prismenkonstante für {target_id} auf {new_value_mm:.1f} mm gesetzt") - - def on_control_point_toggled(self, station_id, point_name, state): - is_active = state == Qt.Checked - self.main_window.statusBar().showMessage( - f"Passpunkt {point_name} {'aktiviert' if is_active else 'deaktiviert'}") - def update_points_table(self): if not self.main_window.parser: return @@ -505,13 +567,57 @@ class JXLAnalysisTab(QWidget): if reply == QMessageBox.Yes: self.main_window.parser.remove_point(name) self.update_display() + + def show_calculation_protocol(self): + """Zeigt das Berechnungsprotokoll in einem Dialog""" + if not self.main_window.parser: + QMessageBox.warning(self, "Fehler", "Bitte zuerst eine JXL-Datei laden!") + return + + protocol = self.main_window.parser.get_calculation_protocol() + + dialog = QDialog(self) + dialog.setWindowTitle("Berechnungsprotokoll") + dialog.setMinimumSize(900, 700) + + layout = QVBoxLayout(dialog) + + text_edit = QTextEdit() + text_edit.setReadOnly(True) + text_edit.setFont(QFont("Courier", 9)) + text_edit.setText(protocol) + layout.addWidget(text_edit) + + # Buttons + btn_layout = QHBoxLayout() + + export_btn = QPushButton("💾 Exportieren") + export_btn.clicked.connect(lambda: export_text_with_dialog(dialog, protocol, "berechnungsprotokoll")) + btn_layout.addWidget(export_btn) + + close_btn = QPushButton("Schließen") + close_btn.clicked.connect(dialog.close) + btn_layout.addWidget(close_btn) + + layout.addLayout(btn_layout) + + dialog.exec_() + + def export_calculation_protocol(self): + """Exportiert das Berechnungsprotokoll""" + if not self.main_window.parser: + QMessageBox.warning(self, "Fehler", "Bitte zuerst eine JXL-Datei laden!") + return + + protocol = self.main_window.parser.get_calculation_protocol() + export_text_with_dialog(self, protocol, "berechnungsprotokoll") # ============================================================================= -# COR-Generator Tab +# COR-Generator Tab (NUR aus ComputedGrid) # ============================================================================= class CORGeneratorTab(QWidget): - """Tab für COR-Datei Generierung""" + """Tab für COR-Datei Generierung - Nur aus berechneten Koordinaten""" def __init__(self, parent=None): super().__init__(parent) @@ -522,19 +628,24 @@ class CORGeneratorTab(QWidget): def setup_ui(self): layout = QVBoxLayout(self) - # Optionen - options_group = QGroupBox("Generierungsoptionen") - options_layout = QGridLayout(options_group) + # Info + info_group = QGroupBox("COR-Generator") + info_layout = QVBoxLayout(info_group) - options_layout.addWidget(QLabel("Methode:"), 0, 0) - self.method_combo = QComboBox() - self.method_combo.addItems([ - "Aus berechneten Koordinaten (ComputedGrid)", - "Aus Rohbeobachtungen berechnen" - ]) - options_layout.addWidget(self.method_combo, 0, 1) + info_label = QLabel( + "💡 Generiert Punktdateien aus den berechneten Koordinaten (ComputedGrid) der JXL-Datei.\n" + "Die Koordinaten werden direkt aus Trimble Access übernommen." + ) + info_label.setStyleSheet("color: #666; background-color: #f0f0f0; padding: 10px;") + info_label.setWordWrap(True) + info_layout.addWidget(info_label) - layout.addWidget(options_group) + # Option: Ausgeglichene Punkte verwenden + self.use_adjusted_check = QCheckBox("✓ Ausgeglichene Punkte verwenden (falls verfügbar)") + self.use_adjusted_check.setEnabled(False) + info_layout.addWidget(self.use_adjusted_check) + + layout.addWidget(info_group) # Generieren Button generate_btn = QPushButton("Punkte generieren") @@ -566,6 +677,22 @@ class CORGeneratorTab(QWidget): export_btn.setStyleSheet("background-color: #2196F3; color: white; font-weight: bold; font-size: 14px; padding: 10px;") layout.addWidget(export_btn) + def showEvent(self, event): + """Wird aufgerufen wenn Tab angezeigt wird""" + super().showEvent(event) + self.update_adjusted_points_status() + + def update_adjusted_points_status(self): + """Aktualisiert den Status der ausgeglichenen Punkte""" + if adjusted_points_store.is_available(): + self.use_adjusted_check.setEnabled(True) + self.use_adjusted_check.setText( + f"✓ Ausgeglichene Punkte verwenden ({len(adjusted_points_store.points)} Punkte)") + else: + self.use_adjusted_check.setEnabled(False) + self.use_adjusted_check.setChecked(False) + self.use_adjusted_check.setText("✓ Ausgeglichene Punkte verwenden (nicht verfügbar)") + def generate_cor(self): if not self.main_window.parser: QMessageBox.warning(self, "Fehler", "Bitte zuerst eine JXL-Datei laden!") @@ -573,21 +700,26 @@ class CORGeneratorTab(QWidget): self.cor_generator = CORGenerator(self.main_window.parser) - if self.method_combo.currentIndex() == 0: - points = self.cor_generator.generate_from_computed_grid() + # Verwende ausgeglichene Punkte wenn aktiviert + if self.use_adjusted_check.isChecked() and adjusted_points_store.is_available(): + points = [] + for name, (x, y, z) in adjusted_points_store.get_points().items(): + points.append(CORPoint(name=name, x=x, y=y, z=z)) + self.cor_generator.cor_points = points else: - points = self.cor_generator.compute_from_observations() + # Aus ComputedGrid generieren (einzige korrekte Methode) + points = self.cor_generator.generate_from_computed_grid() # Tabelle aktualisieren - self.preview_table.setRowCount(len(points)) - for row, p in enumerate(points): + self.preview_table.setRowCount(len(self.cor_generator.cor_points)) + for row, p in enumerate(self.cor_generator.cor_points): self.preview_table.setItem(row, 0, QTableWidgetItem(p.name)) self.preview_table.setItem(row, 1, QTableWidgetItem(f"{p.x:.4f}")) self.preview_table.setItem(row, 2, QTableWidgetItem(f"{p.y:.4f}")) self.preview_table.setItem(row, 3, QTableWidgetItem(f"{p.z:.4f}")) self.stats_text.setText(self.cor_generator.get_statistics()) - self.main_window.statusBar().showMessage(f"{len(points)} Punkte generiert") + self.main_window.statusBar().showMessage(f"{len(self.cor_generator.cor_points)} Punkte generiert") def export_with_dialog(self): if not self.cor_generator or not self.cor_generator.cor_points: @@ -598,10 +730,10 @@ class CORGeneratorTab(QWidget): # ============================================================================= -# Transformation Tab (Y-Richtung entfernt) +# Transformation Tab # ============================================================================= class TransformationTab(QWidget): - """Tab für Koordinatentransformation - Y-Richtung entfernt""" + """Tab für Koordinatentransformation""" def __init__(self, parent=None): super().__init__(parent) @@ -612,6 +744,20 @@ class TransformationTab(QWidget): def setup_ui(self): layout = QVBoxLayout(self) + # Datenquelle + source_group = QGroupBox("Datenquelle") + source_layout = QVBoxLayout(source_group) + + self.source_jxl_radio = QRadioButton("Aus JXL-Datei") + self.source_jxl_radio.setChecked(True) + source_layout.addWidget(self.source_jxl_radio) + + self.source_adjusted_radio = QRadioButton("Ausgeglichene Punkte verwenden") + self.source_adjusted_radio.setEnabled(False) + source_layout.addWidget(self.source_adjusted_radio) + + layout.addWidget(source_group) + # Methode auswählen method_group = QGroupBox("Transformationsmethode") method_layout = QVBoxLayout(method_group) @@ -659,21 +805,9 @@ class TransformationTab(QWidget): manual_layout.addWidget(self.rotation_spin, 3, 1) manual_layout.addWidget(QLabel("gon"), 3, 2) - manual_layout.addWidget(QLabel("Drehpunkt X:"), 4, 0) - self.pivot_x_spin = QDoubleSpinBox() - self.pivot_x_spin.setRange(-1000000, 1000000) - self.pivot_x_spin.setDecimals(4) - manual_layout.addWidget(self.pivot_x_spin, 4, 1) - - manual_layout.addWidget(QLabel("Drehpunkt Y:"), 5, 0) - self.pivot_y_spin = QDoubleSpinBox() - self.pivot_y_spin.setRange(-1000000, 1000000) - self.pivot_y_spin.setDecimals(4) - manual_layout.addWidget(self.pivot_y_spin, 5, 1) - layout.addWidget(self.manual_group) - # 2-Punkte-Definition (OHNE Y-Richtung!) + # 2-Punkte-Definition self.twopoint_group = QGroupBox("2-Punkte-Definition") twopoint_layout = QGridLayout(self.twopoint_group) @@ -692,17 +826,20 @@ class TransformationTab(QWidget): self.twopoint_group.setVisible(False) layout.addWidget(self.twopoint_group) - # Transformation berechnen + # Buttons + btn_layout = QHBoxLayout() + 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) + btn_layout.addWidget(transform_btn) - # Anwenden Button - apply_btn = QPushButton("Transformation anwenden (Punktliste aktualisieren)") + apply_btn = QPushButton("Auf Punkte anwenden") apply_btn.clicked.connect(self.apply_transformation) apply_btn.setStyleSheet("background-color: #2196F3; color: white; font-weight: bold;") - layout.addWidget(apply_btn) + btn_layout.addWidget(apply_btn) + + layout.addLayout(btn_layout) # Ergebnisse results_group = QGroupBox("Ergebnisse") @@ -712,20 +849,25 @@ class TransformationTab(QWidget): self.results_text.setReadOnly(True) results_layout.addWidget(self.results_text) - # Export - export_layout = QHBoxLayout() - export_report_btn = QPushButton("Bericht exportieren") - export_report_btn.clicked.connect(self.export_report) - export_layout.addWidget(export_report_btn) - export_points_btn = QPushButton("📥 Punkte exportieren...") export_points_btn.clicked.connect(self.export_points) - export_layout.addWidget(export_points_btn) - - results_layout.addLayout(export_layout) + results_layout.addWidget(export_points_btn) layout.addWidget(results_group) + def showEvent(self, event): + super().showEvent(event) + self.update_adjusted_points_status() + + def update_adjusted_points_status(self): + if adjusted_points_store.is_available(): + self.source_adjusted_radio.setEnabled(True) + self.source_adjusted_radio.setText( + f"Ausgeglichene Punkte verwenden ({len(adjusted_points_store.points)} Punkte)") + else: + self.source_adjusted_radio.setEnabled(False) + self.source_jxl_radio.setChecked(True) + def toggle_method(self): self.manual_group.setVisible(self.manual_radio.isChecked()) self.twopoint_group.setVisible(self.twopoint_radio.isChecked()) @@ -742,42 +884,29 @@ class TransformationTab(QWidget): self.xy_origin_combo.clear() self.z_origin_combo.clear() - default_xy = None - default_z = None - for name in sorted(points): self.xy_origin_combo.addItem(name) self.z_origin_combo.addItem(name) - - if name == "7001": - default_xy = name - if name == "7002": - default_z = name - - if default_xy: - idx = self.xy_origin_combo.findText(default_xy) - if idx >= 0: - self.xy_origin_combo.setCurrentIndex(idx) - - if default_z: - idx = self.z_origin_combo.findText(default_z) - if idx >= 0: - self.z_origin_combo.setCurrentIndex(idx) def execute_transformation(self): if not self.main_window.parser: QMessageBox.warning(self, "Fehler", "Bitte zuerst eine JXL-Datei laden!") return + # Punkte sammeln 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.0 - )) + if self.source_adjusted_radio.isChecked() and adjusted_points_store.is_available(): + for name, (x, y, z) in adjusted_points_store.get_points().items(): + points.append(CORPoint(name=name, x=x, y=y, z=z)) + else: + 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.0 + )) self.transformer.set_points(points) @@ -787,14 +916,13 @@ class TransformationTab(QWidget): dy=self.dy_spin.value(), dz=self.dz_spin.value(), rotation_gon=self.rotation_spin.value(), - pivot_x=self.pivot_x_spin.value(), - pivot_y=self.pivot_y_spin.value() + pivot_x=0, + pivot_y=0 ) elif self.twopoint_radio.isChecked(): origin = self.xy_origin_combo.currentText() zref = self.z_origin_combo.currentText() - # Transformation zum Nullpunkt (nur Translation, keine Rotation) if not self.transformer.compute_translation_only(origin, zref): QMessageBox.warning(self, "Fehler", "Punkt nicht gefunden!") return @@ -838,16 +966,6 @@ class TransformationTab(QWidget): QMessageBox.information(self, "Erfolg", f"{len(self.transformer.transformed_points)} Punkte transformiert!") - - self.main_window.statusBar().showMessage("Transformation angewendet") - - def export_report(self): - file_path, _ = QFileDialog.getSaveFileName( - self, "Bericht speichern", "", "Text Files (*.txt)") - if file_path: - with open(file_path, 'w', encoding='utf-8') as f: - f.write(self.results_text.toPlainText()) - QMessageBox.information(self, "Erfolg", f"Bericht gespeichert: {file_path}") def export_points(self): if not self.transformer.transformed_points: @@ -858,50 +976,68 @@ class TransformationTab(QWidget): # ============================================================================= -# Georeferenzierung Tab (KOMPLETT NEU) +# Georeferenzierung Tab (mit automatischer Punktzuordnung) # ============================================================================= class GeoreferencingTab(QWidget): - """Tab für Georeferenzierung - NEUER WORKFLOW mit Punktdatei-Laden""" + """Tab für Georeferenzierung - Mit automatischer Punktzuordnung über Tripel""" 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) + # Datenquelle + source_group = QGroupBox("Datenquelle für Ist-Koordinaten") + source_layout = QVBoxLayout(source_group) + + self.source_jxl_radio = QRadioButton("Aus JXL-Datei") + self.source_jxl_radio.setChecked(True) + source_layout.addWidget(self.source_jxl_radio) + + self.source_adjusted_radio = QRadioButton("Ausgeglichene Punkte verwenden") + self.source_adjusted_radio.setEnabled(False) + source_layout.addWidget(self.source_adjusted_radio) + + layout.addWidget(source_group) + # Schritt 1: Punktdatei laden load_group = QGroupBox("Schritt 1: Soll-Koordinaten laden") - load_layout = QVBoxLayout(load_group) + load_layout = QHBoxLayout(load_group) - 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) + load_layout.addWidget(self.load_file_btn) 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() + load_layout.addWidget(self.loaded_file_label) + load_layout.addStretch() - load_layout.addLayout(load_btn_layout) layout.addWidget(load_group) - # Schritt 2: Punkt-Zuordnungstabelle - assign_group = QGroupBox("Schritt 2: Punkt-Zuordnung (Soll → Ist aus JXL)") + # Schritt 2: Punkt-Zuordnung + assign_group = QGroupBox("Schritt 2: Punkt-Zuordnung (Soll → Ist)") assign_layout = QVBoxLayout(assign_group) - 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) + # Automatische Zuordnung Button + auto_layout = QHBoxLayout() + auto_btn = QPushButton("🔍 Automatische Zuordnung (Tripel-Analyse)") + auto_btn.clicked.connect(self.auto_assign_points) + auto_btn.setStyleSheet("background-color: #9C27B0; color: white; font-weight: bold;") + auto_layout.addWidget(auto_btn) + self.auto_result_label = QLabel("") + auto_layout.addWidget(self.auto_result_label) + auto_layout.addStretch() + assign_layout.addLayout(auto_layout) + + # Zuordnungstabelle self.assign_table = QTableWidget() self.assign_table.setColumnCount(8) self.assign_table.setHorizontalHeaderLabels([ @@ -914,17 +1050,17 @@ class GeoreferencingTab(QWidget): layout.addWidget(assign_group) # Schritt 3: Berechnung - calc_group = QGroupBox("Schritt 3: Georeferenzierung durchführen") + calc_group = QGroupBox("Schritt 3: Georeferenzierung") calc_layout = QHBoxLayout(calc_group) calc_btn = QPushButton("🔄 Transformation berechnen") calc_btn.clicked.connect(self.calculate_transformation) - calc_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold; font-size: 14px; padding: 10px;") + calc_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;") 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;") + apply_btn.setStyleSheet("background-color: #2196F3; color: white;") calc_layout.addWidget(apply_btn) layout.addWidget(calc_group) @@ -938,21 +1074,37 @@ class GeoreferencingTab(QWidget): self.results_text.setFont(QFont("Courier", 9)) results_layout.addWidget(self.results_text) - # Export - export_layout = QHBoxLayout() - - export_report_btn = QPushButton("Bericht exportieren") - export_report_btn.clicked.connect(self.export_report) - export_layout.addWidget(export_report_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) + export_btn = QPushButton("📥 Punkte exportieren...") + export_btn.clicked.connect(self.export_transformed_points) + results_layout.addWidget(export_btn) layout.addWidget(results_group) + def showEvent(self, event): + super().showEvent(event) + self.update_adjusted_points_status() + + def update_adjusted_points_status(self): + if adjusted_points_store.is_available(): + self.source_adjusted_radio.setEnabled(True) + self.source_adjusted_radio.setText( + f"Ausgeglichene Punkte verwenden ({len(adjusted_points_store.points)} Punkte)") + else: + self.source_adjusted_radio.setEnabled(False) + self.source_jxl_radio.setChecked(True) + + def get_ist_points(self): + """Gibt die Ist-Punkte zurück (aus JXL oder ausgeglichene)""" + if self.source_adjusted_radio.isChecked() and adjusted_points_store.is_available(): + return adjusted_points_store.get_points() + elif self.main_window.parser: + result = {} + for name, p in self.main_window.parser.get_active_points().items(): + if p.east is not None and p.north is not None: + result[name] = (p.east, p.north, p.elevation or 0.0) + return result + return {} + def load_target_file(self): """Lädt eine COR/CSV-Datei mit Soll-Koordinaten""" file_path, _ = QFileDialog.getOpenFileName( @@ -973,7 +1125,6 @@ class GeoreferencingTab(QWidget): if not line or line.startswith('#'): continue - # Komma oder Semikolon als Trenner parts = line.replace(';', ',').split(',') if len(parts) >= 4: name = parts[0].strip() @@ -983,17 +1134,14 @@ class GeoreferencingTab(QWidget): z = float(parts[3].strip()) self.loaded_target_points[name] = (x, y, z) except ValueError: - continue # Header oder ungültige Zeile überspringen + continue - self.loaded_file_label.setText(f"✓ {os.path.basename(file_path)} ({len(self.loaded_target_points)} Punkte)") + 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;") - # 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}") @@ -1001,26 +1149,20 @@ class GeoreferencingTab(QWidget): """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())) + ist_points = self.get_ist_points() + jxl_names = ["-- Nicht zugeordnet --"] + sorted(ist_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) + combo.addItems(jxl_names) - # Automatische Zuordnung: gleicher Name? - if name in jxl_points: + # Automatische Zuordnung bei gleichem Namen + if name in jxl_names: idx = combo.findText(name) if idx >= 0: combo.setCurrentIndex(idx) @@ -1029,41 +1171,126 @@ class GeoreferencingTab(QWidget): 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: + ist_points = self.get_ist_points() + + if jxl_name == "-- Nicht zugeordnet --" or jxl_name not in ist_points: 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 "")) + x, y, z = ist_points[jxl_name] + self.assign_table.setItem(row, 5, QTableWidgetItem(f"{x:.4f}")) + self.assign_table.setItem(row, 6, QTableWidgetItem(f"{y:.4f}")) + self.assign_table.setItem(row, 7, QTableWidgetItem(f"{z:.4f}")) + + def auto_assign_points(self): + """Automatische Punktzuordnung basierend auf Tripel-Analyse""" + if len(self.loaded_target_points) < 3: + QMessageBox.warning(self, "Fehler", "Mindestens 3 Soll-Punkte erforderlich!") + return + + ist_points = self.get_ist_points() + if len(ist_points) < 3: + QMessageBox.warning(self, "Fehler", "Mindestens 3 Ist-Punkte erforderlich!") + return + + # Berechne Distanzen für Soll-Punkte + soll_names = list(self.loaded_target_points.keys()) + soll_distances = {} + for i, name1 in enumerate(soll_names): + for name2 in soll_names[i+1:]: + x1, y1, _ = self.loaded_target_points[name1] + x2, y2, _ = self.loaded_target_points[name2] + dist = math.sqrt((x2-x1)**2 + (y2-y1)**2) + soll_distances[(name1, name2)] = dist + soll_distances[(name2, name1)] = dist + + # Berechne Distanzen für Ist-Punkte + ist_names = list(ist_points.keys()) + ist_distances = {} + for i, name1 in enumerate(ist_names): + for name2 in ist_names[i+1:]: + x1, y1, _ = ist_points[name1] + x2, y2, _ = ist_points[name2] + dist = math.sqrt((x2-x1)**2 + (y2-y1)**2) + ist_distances[(name1, name2)] = dist + ist_distances[(name2, name1)] = dist + + # Finde beste Zuordnung über Tripel + best_assignment = None + best_rmse = float('inf') + + # Alle möglichen Tripel aus Soll-Punkten + for soll_tripel in combinations(soll_names, 3): + # Alle möglichen Tripel aus Ist-Punkten + for ist_tripel in combinations(ist_names, 3): + # Berechne RMSE für diese Zuordnung + rmse = self._compute_tripel_rmse( + soll_tripel, ist_tripel, soll_distances, ist_distances) + + if rmse < best_rmse: + best_rmse = rmse + best_assignment = dict(zip(soll_tripel, ist_tripel)) + + if best_assignment and best_rmse < 0.5: # Max 0.5m Abweichung + # Zuordnung in Tabelle übernehmen + for row in range(self.assign_table.rowCount()): + soll_name = self.assign_table.item(row, 0).text() + combo = self.assign_table.cellWidget(row, 4) + + if soll_name in best_assignment: + ist_name = best_assignment[soll_name] + idx = combo.findText(ist_name) + if idx >= 0: + combo.setCurrentIndex(idx) + + self.auto_result_label.setText( + f"✓ Zuordnung gefunden (RMSE: {best_rmse*1000:.1f} mm)") + self.auto_result_label.setStyleSheet("color: green; font-weight: bold;") + else: + self.auto_result_label.setText("❌ Keine passende Zuordnung gefunden") + self.auto_result_label.setStyleSheet("color: red;") + + def _compute_tripel_rmse(self, soll_tripel, ist_tripel, soll_dist, ist_dist): + """Berechnet RMSE für eine Tripel-Zuordnung""" + errors = [] + for i in range(3): + for j in range(i+1, 3): + s1, s2 = soll_tripel[i], soll_tripel[j] + i1, i2 = ist_tripel[i], ist_tripel[j] + + d_soll = soll_dist.get((s1, s2), 0) + d_ist = ist_dist.get((i1, i2), 0) + + if d_soll > 0 and d_ist > 0: + errors.append((d_soll - d_ist) ** 2) + + if errors: + return math.sqrt(sum(errors) / len(errors)) + return float('inf') def calculate_transformation(self): - """Berechnet die Transformation basierend auf den Punkt-Paaren""" + """Berechnet die Transformation""" 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!") + ist_points = self.get_ist_points() + if not ist_points: + QMessageBox.warning(self, "Fehler", "Keine Ist-Punkte verfügbar!") return self.georeferencer.clear_control_points() - # Punkt-Paare sammeln valid_pairs = 0 for row in range(self.assign_table.rowCount()): combo = self.assign_table.cellWidget(row, 4) @@ -1074,24 +1301,16 @@ class GeoreferencingTab(QWidget): 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: + if jxl_name not in ist_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 + local_x, local_y, local_z = ist_points[jxl_name] self.georeferencer.add_control_point( jxl_name, local_x, local_y, local_z, target_x, target_y, target_z @@ -1100,8 +1319,7 @@ class GeoreferencingTab(QWidget): if valid_pairs < 2: QMessageBox.warning(self, "Fehler", - f"Mindestens 2 gültige Punkt-Paare erforderlich!\n" - f"Aktuell: {valid_pairs}") + f"Mindestens 2 gültige Punkt-Paare erforderlich! Aktuell: {valid_pairs}") return try: @@ -1114,7 +1332,7 @@ class GeoreferencingTab(QWidget): QMessageBox.critical(self, "Fehler", f"Berechnung fehlgeschlagen: {e}") def apply_to_all_points(self): - """Wendet die Transformation auf alle JXL-Punkte an""" + """Wendet die Transformation auf alle Punkte an""" if self.georeferencer.result is None: QMessageBox.warning(self, "Fehler", "Bitte zuerst Transformation berechnen!") return @@ -1130,18 +1348,12 @@ class GeoreferencingTab(QWidget): 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 - )) + ist_points = self.get_ist_points() + points = [CORPoint(name=n, x=x, y=y, z=z) for n, (x, y, z) in ist_points.items()] 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] @@ -1149,39 +1361,19 @@ class GeoreferencingTab(QWidget): 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)") - if file_path: - with open(file_path, 'w', encoding='utf-8') as f: - f.write(self.results_text.toPlainText()) - QMessageBox.information(self, "Erfolg", f"Bericht gespeichert: {file_path}") + QMessageBox.information(self, "Erfolg", f"{len(transformed)} Punkte georeferenziert!") def export_transformed_points(self): if self.georeferencer.result is None: QMessageBox.warning(self, "Fehler", "Bitte zuerst Transformation berechnen!") return - if not self.main_window.parser: - return - - # 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 - )) + ist_points = self.get_ist_points() + points = [CORPoint(name=n, x=x, y=y, z=z) for n, (x, y, z) in ist_points.items()] self.georeferencer.set_points_to_transform(points) transformed = self.georeferencer.transform_points() @@ -1190,17 +1382,23 @@ class GeoreferencingTab(QWidget): # ============================================================================= -# Netzausgleichung Tab +# Netzausgleichung Tab (KOMPLETT ÜBERARBEITET) # ============================================================================= class NetworkAdjustmentTab(QWidget): - """Tab für Netzausgleichung""" + """ + Tab für Netzausgleichung - KORREKTES KONZEPT: + - Festpunkte = Passpunkte (5001, 5002, etc.) - werden NICHT ausgeglichen + - Neupunkte = Standpunkte (1001, 1002, etc.) - werden ausgeglichen + - Messpunkte = 3000er Serie - werden ausgeglichen + """ def __init__(self, parent=None): super().__init__(parent) self.main_window = parent self.adjustment = None - self.fixed_points = set() - self.measurement_points = set() + self.fixed_points = set() # Passpunkte (nicht ausgeglichen) + self.new_points = set() # Standpunkte (ausgeglichen) + self.measurement_points = set() # Messpunkte (ausgeglichen) self.setup_ui() def setup_ui(self): @@ -1223,46 +1421,50 @@ class NetworkAdjustmentTab(QWidget): self.convergence_spin.setValue(0.01) config_layout.addWidget(self.convergence_spin, 1, 1) - config_layout.addWidget(QLabel("Sigma-0 a-priori:"), 2, 0) - self.sigma0_spin = QDoubleSpinBox() - self.sigma0_spin.setRange(0.1, 10) - self.sigma0_spin.setDecimals(2) - self.sigma0_spin.setValue(1.0) - config_layout.addWidget(self.sigma0_spin, 2, 1) - layout.addWidget(config_group) - # Automatisch erkannte Punkte - points_group = QGroupBox("Automatisch erkannte Punkttypen") + # Punktklassifikation + points_group = QGroupBox("Punktklassifikation (KORREKTES KONZEPT)") points_layout = QVBoxLayout(points_group) info_label = QLabel( - "💡 Automatische Erkennung:\n" - " • Festpunkte: Stationspunkte und Anschlusspunkte\n" - " • Messpunkte: 3000er Punkte" + "💡 KORREKTES KONZEPT:\n" + " • Festpunkte (grün): Passpunkte mit bekannten Koordinaten - werden NICHT ausgeglichen\n" + " • Neupunkte (blau): Standpunkte des Tachymeters - werden AUSGEGLICHEN\n" + " • Messpunkte (gelb): Detailpunkte - werden AUSGEGLICHEN" ) - info_label.setStyleSheet("color: #666; background-color: #f0f0f0; padding: 10px;") + info_label.setStyleSheet("background-color: #f0f0f0; padding: 10px;") points_layout.addWidget(info_label) self.points_table = QTableWidget() - self.points_table.setColumnCount(4) - self.points_table.setHorizontalHeaderLabels(["Punkt", "Typ", "X", "Y"]) + self.points_table.setColumnCount(5) + self.points_table.setHorizontalHeaderLabels(["Punkt", "Typ", "X", "Y", "Z"]) self.points_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - self.points_table.setMaximumHeight(150) + self.points_table.setMaximumHeight(200) points_layout.addWidget(self.points_table) - refresh_btn = QPushButton("Punkte automatisch erkennen") - refresh_btn.clicked.connect(self.auto_detect_points) - refresh_btn.setStyleSheet("background-color: #FF9800; color: white;") + refresh_btn = QPushButton("🔍 Punkte automatisch klassifizieren") + refresh_btn.clicked.connect(self.auto_classify_points) + refresh_btn.setStyleSheet("background-color: #FF9800; color: white; font-weight: bold;") points_layout.addWidget(refresh_btn) layout.addWidget(points_group) - # Ausgleichung durchführen - adjust_btn = QPushButton("Netzausgleichung durchführen") + # Ausgleichung + btn_layout = QHBoxLayout() + + adjust_btn = QPushButton("📐 Netzausgleichung durchführen") adjust_btn.clicked.connect(self.run_adjustment) - adjust_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;") - layout.addWidget(adjust_btn) + adjust_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold; font-size: 14px; padding: 10px;") + btn_layout.addWidget(adjust_btn) + + self.adopt_btn = QPushButton("✓ Ausgeglichene Punkte übernehmen") + self.adopt_btn.clicked.connect(self.adopt_adjusted_points) + self.adopt_btn.setStyleSheet("background-color: #2196F3; color: white; font-weight: bold;") + self.adopt_btn.setEnabled(False) + btn_layout.addWidget(self.adopt_btn) + + layout.addLayout(btn_layout) # Ergebnisse results_group = QGroupBox("Ergebnisse") @@ -1273,7 +1475,6 @@ class NetworkAdjustmentTab(QWidget): self.results_text.setFont(QFont("Courier", 9)) results_layout.addWidget(self.results_text) - # Export export_layout = QHBoxLayout() export_report_btn = QPushButton("Bericht exportieren") @@ -1287,81 +1488,100 @@ class NetworkAdjustmentTab(QWidget): results_layout.addLayout(export_layout) layout.addWidget(results_group) - def auto_detect_points(self): + def auto_classify_points(self): + """Klassifiziert Punkte automatisch nach dem korrekten Konzept""" if not self.main_window.parser: QMessageBox.warning(self, "Fehler", "Bitte zuerst eine JXL-Datei laden!") return parser = self.main_window.parser self.fixed_points.clear() + self.new_points.clear() self.measurement_points.clear() - for station_id, station in parser.stations.items(): - if station.name: - self.fixed_points.add(station.name) - - for bb_id, bb in parser.backbearings.items(): - if bb.station_record_id == station_id and bb.backsight: - self.fixed_points.add(bb.backsight) - - 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) + # 1. FESTPUNKTE = Passpunkte (Referenzpunkte mit bekannten Koordinaten) + ref_points = parser.get_reference_points() + for name in ref_points: + if name in parser.points: + self.fixed_points.add(name) + # 2. NEUPUNKTE = Standpunkte (Stationen des Tachymeters) + station_points = parser.get_station_points() + for name in station_points: + if name not in self.fixed_points: # Nicht wenn es ein Passpunkt ist + self.new_points.add(name) + + # 3. MESSPUNKTE = Alle anderen Punkte for name in parser.get_active_points().keys(): - if name.startswith("3"): + if name not in self.fixed_points and name not in self.new_points: self.measurement_points.add(name) self.update_points_table() self.main_window.statusBar().showMessage( - f"Erkannt: {len(self.fixed_points)} Festpunkte, {len(self.measurement_points)} Messpunkte") + f"Klassifiziert: {len(self.fixed_points)} Festpunkte, " + f"{len(self.new_points)} Neupunkte, " + f"{len(self.measurement_points)} Messpunkte") def update_points_table(self): + """Aktualisiert die Punkttabelle""" parser = self.main_window.parser if not parser: return - all_points = list(self.fixed_points) + list(self.measurement_points) - self.points_table.setRowCount(len(all_points)) + all_classified = [] + for name in self.fixed_points: + all_classified.append((name, "Festpunkt", QColor(200, 255, 200))) + for name in self.new_points: + all_classified.append((name, "Neupunkt", QColor(200, 200, 255))) + for name in self.measurement_points: + all_classified.append((name, "Messpunkt", QColor(255, 255, 200))) - for row, name in enumerate(sorted(all_points)): - self.points_table.setItem(row, 0, QTableWidgetItem(name)) + self.points_table.setRowCount(len(all_classified)) + + for row, (name, point_type, color) in enumerate(sorted(all_classified)): + name_item = QTableWidgetItem(name) + name_item.setBackground(QBrush(color)) + self.points_table.setItem(row, 0, name_item) - if name in self.fixed_points: - type_item = QTableWidgetItem("Festpunkt") - type_item.setBackground(QBrush(QColor(200, 230, 200))) - else: - type_item = QTableWidgetItem("Messpunkt") - type_item.setBackground(QBrush(QColor(200, 200, 230))) + type_item = QTableWidgetItem(point_type) + type_item.setBackground(QBrush(color)) self.points_table.setItem(row, 1, type_item) if name in parser.points: p = parser.points[name] - self.points_table.setItem(row, 2, QTableWidgetItem(f"{p.east:.4f}" if p.east else "")) - self.points_table.setItem(row, 3, QTableWidgetItem(f"{p.north:.4f}" if p.north else "")) + x_item = QTableWidgetItem(f"{p.east:.4f}" if p.east else "") + y_item = QTableWidgetItem(f"{p.north:.4f}" if p.north else "") + z_item = QTableWidgetItem(f"{p.elevation:.4f}" if p.elevation else "") + x_item.setBackground(QBrush(color)) + y_item.setBackground(QBrush(color)) + z_item.setBackground(QBrush(color)) + self.points_table.setItem(row, 2, x_item) + self.points_table.setItem(row, 3, y_item) + self.points_table.setItem(row, 4, z_item) def run_adjustment(self): + """Führt die Netzausgleichung durch""" if not self.main_window.parser: QMessageBox.warning(self, "Fehler", "Bitte zuerst eine JXL-Datei laden!") return - if not self.fixed_points and not self.measurement_points: - self.auto_detect_points() + if not self.fixed_points and not self.new_points and not self.measurement_points: + self.auto_classify_points() if not self.fixed_points: - QMessageBox.warning(self, "Fehler", "Keine Festpunkte erkannt!") + QMessageBox.warning(self, "Fehler", + "Keine Festpunkte erkannt! Mindestens ein Passpunkt mit bekannten Koordinaten wird benötigt.") return 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() self.adjustment.extract_observations() self.adjustment.initialize_points() + # NUR Festpunkte setzen - Neupunkte und Messpunkte werden ausgeglichen! for point_name in self.fixed_points: self.adjustment.set_fixed_point(point_name) @@ -1370,6 +1590,8 @@ class NetworkAdjustmentTab(QWidget): report = self.create_detailed_report() self.results_text.setText(report) + self.adopt_btn.setEnabled(True) + status = "konvergiert" if result.converged else "nicht konvergiert" self.main_window.statusBar().showMessage( f"Ausgleichung {status}, {result.iterations} Iterationen") @@ -1380,44 +1602,105 @@ class NetworkAdjustmentTab(QWidget): traceback.print_exc() def create_detailed_report(self): + """Erstellt einen detaillierten Bericht""" if not self.adjustment or not self.adjustment.result: return "Keine Ergebnisse." lines = [] - lines.append("=" * 80) - lines.append("NETZAUSGLEICHUNG - ERGEBNISBERICHT") - lines.append("=" * 80) + lines.append("=" * 90) + lines.append("NETZAUSGLEICHUNG - ERGEBNISBERICHT (KORREKTES KONZEPT)") + lines.append("=" * 90) lines.append("") - 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("KONZEPT:") + lines.append(" • Festpunkte = Passpunkte mit bekannten Koordinaten (NICHT ausgeglichen)") + lines.append(" • Neupunkte = Standpunkte des Tachymeters (AUSGEGLICHEN)") + lines.append(" • Messpunkte = Detailpunkte (AUSGEGLICHEN)") + lines.append("") + lines.append("-" * 90) + lines.append("STATISTIK") + lines.append("-" * 90) + lines.append(f"Festpunkte (Passpunkte): {len(self.fixed_points)}") + lines.append(f"Neupunkte (Standpunkte): {len(self.new_points)}") + lines.append(f"Messpunkte: {len(self.measurement_points)}") + lines.append(f"Beobachtungen: {self.adjustment.result.num_observations}") + lines.append(f"Unbekannte: {self.adjustment.result.num_unknowns}") + lines.append(f"Redundanz: {self.adjustment.result.redundancy}") + 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-posteriori: {self.adjustment.result.sigma_0_posteriori:.4f}") lines.append("") - lines.append("-" * 80) - lines.append(f"{'Punkt':<12} {'Typ':<12} {'X [m]':>14} {'Y [m]':>14} {'Z [m]':>12}") - lines.append("-" * 80) - for name in sorted(list(self.fixed_points) + list(self.measurement_points)): + # Festpunkte (unverändert) + lines.append("-" * 90) + lines.append("FESTPUNKTE (Passpunkte - NICHT ausgeglichen)") + lines.append("-" * 90) + lines.append(f"{'Punkt':<12} {'X [m]':>14} {'Y [m]':>14} {'Z [m]':>12}") + lines.append("-" * 90) + for name in sorted(self.fixed_points): if name in self.adjustment.points: p = self.adjustment.points[name] - 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}") + lines.append(f"{name:<12} {p.x:>14.4f} {p.y:>14.4f} {p.z:>12.4f}") + lines.append("") + + # Neupunkte (ausgeglichen) + lines.append("-" * 90) + lines.append("NEUPUNKTE (Standpunkte - AUSGEGLICHEN)") + lines.append("-" * 90) + lines.append(f"{'Punkt':<12} {'X [m]':>14} {'Y [m]':>14} {'Z [m]':>12} {'σX [mm]':>10} {'σY [mm]':>10} {'σPos [mm]':>10}") + lines.append("-" * 90) + for name in sorted(self.new_points): + if name in self.adjustment.points: + p = self.adjustment.points[name] + lines.append(f"{name:<12} {p.x:>14.4f} {p.y:>14.4f} {p.z:>12.4f} " + f"{p.std_x*1000:>10.2f} {p.std_y*1000:>10.2f} {p.std_position*1000:>10.2f}") + lines.append("") + + # Messpunkte (ausgeglichen) + lines.append("-" * 90) + lines.append("MESSPUNKTE (AUSGEGLICHEN)") + lines.append("-" * 90) + lines.append(f"{'Punkt':<12} {'X [m]':>14} {'Y [m]':>14} {'Z [m]':>12} {'σX [mm]':>10} {'σY [mm]':>10} {'σPos [mm]':>10}") + lines.append("-" * 90) + for name in sorted(self.measurement_points): + if name in self.adjustment.points: + p = self.adjustment.points[name] + lines.append(f"{name:<12} {p.x:>14.4f} {p.y:>14.4f} {p.z:>12.4f} " + f"{p.std_x*1000:>10.2f} {p.std_y*1000:>10.2f} {p.std_position*1000:>10.2f}") + + lines.append("") + lines.append("=" * 90) return "\n".join(lines) + def adopt_adjusted_points(self): + """Übernimmt ausgeglichene Punkte in den globalen Speicher""" + if not self.adjustment or not self.adjustment.points: + QMessageBox.warning(self, "Fehler", "Keine ausgeglichenen Punkte!") + return + + points_dict = {} + for name, p in self.adjustment.points.items(): + points_dict[name] = (p.x, p.y, p.z) + + adjusted_points_store.set_points(points_dict) + + QMessageBox.information(self, "Erfolg", + f"{len(points_dict)} ausgeglichene Punkte übernommen!\n\n" + "Die Punkte sind jetzt in anderen Modulen verfügbar:\n" + "• COR Generator\n" + "• Transformation\n" + "• Georeferenzierung") + + self.main_window.statusBar().showMessage( + f"Ausgeglichene Punkte verfügbar ({len(points_dict)} Punkte)") + def export_report(self): if not self.adjustment: QMessageBox.warning(self, "Fehler", "Keine Ergebnisse!") return - file_path, _ = QFileDialog.getSaveFileName( - self, "Bericht speichern", "", "Text Files (*.txt)") - if file_path: - with open(file_path, 'w', encoding='utf-8') as f: - f.write(self.create_detailed_report()) - QMessageBox.information(self, "Erfolg", f"Gespeichert: {file_path}") + report = self.create_detailed_report() + export_text_with_dialog(self, report, "netzausgleichung_bericht") def export_points(self): if not self.adjustment or not self.adjustment.result: @@ -1504,8 +1787,7 @@ class ReferencePointAdjusterTab(QWidget): layout.addWidget(new_coords_group) # Aktionen - actions_group = QGroupBox("Aktionen") - actions_layout = QHBoxLayout(actions_group) + actions_layout = QHBoxLayout() preview_btn = QPushButton("Vorschau") preview_btn.clicked.connect(self.preview_transformation) @@ -1522,9 +1804,9 @@ class ReferencePointAdjusterTab(QWidget): export_btn.setStyleSheet("background-color: #FF9800; color: white;") actions_layout.addWidget(export_btn) - layout.addWidget(actions_group) + layout.addLayout(actions_layout) - # Vorschau-Tabelle + # Vorschau preview_group = QGroupBox("Vorschau") preview_layout = QVBoxLayout(preview_group) @@ -1665,8 +1947,8 @@ class MainWindow(QMainWindow): def __init__(self): super().__init__() self.parser = None - self.setWindowTitle("Trimble Geodesy Tool v2.1") - self.setMinimumSize(1100, 800) + self.setWindowTitle("Trimble Geodesy Tool v3.0") + self.setMinimumSize(1200, 850) self.setup_ui() self.setup_menu() @@ -1675,6 +1957,11 @@ class MainWindow(QMainWindow): self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) + # Status für ausgeglichene Punkte + self.adjusted_status = QLabel("🔴 Keine ausgeglichenen Punkte verfügbar") + self.adjusted_status.setStyleSheet("background-color: #fff3e0; padding: 5px;") + main_layout.addWidget(self.adjusted_status) + # Tab-Widget self.tabs = QTabWidget() self.tabs.addTab(JXLAnalysisTab(self), "📊 JXL-Analyse") @@ -1684,12 +1971,27 @@ class MainWindow(QMainWindow): self.tabs.addTab(NetworkAdjustmentTab(self), "📐 Netzausgleichung") self.tabs.addTab(ReferencePointAdjusterTab(self), "📍 Referenzpunkt") + self.tabs.currentChanged.connect(self.on_tab_changed) main_layout.addWidget(self.tabs) # Status Bar self.setStatusBar(QStatusBar()) self.statusBar().showMessage("Bereit - Bitte JXL-Datei laden") + def on_tab_changed(self, index): + """Aktualisiert Status bei Tab-Wechsel""" + self.update_adjusted_status() + + def update_adjusted_status(self): + """Aktualisiert die Statusanzeige für ausgeglichene Punkte""" + if adjusted_points_store.is_available(): + n = len(adjusted_points_store.points) + self.adjusted_status.setText(f"🟢 {n} ausgeglichene Punkte verfügbar") + self.adjusted_status.setStyleSheet("background-color: #e8f5e9; padding: 5px; color: green;") + else: + self.adjusted_status.setText("🔴 Keine ausgeglichenen Punkte verfügbar") + self.adjusted_status.setStyleSheet("background-color: #fff3e0; padding: 5px;") + def setup_menu(self): menubar = self.menuBar() @@ -1726,14 +2028,15 @@ class MainWindow(QMainWindow): def show_about(self): QMessageBox.about(self, "Über Trimble Geodesy Tool", - "Trimble Geodesy Tool v2.1\n\n" + "Trimble Geodesy Tool v3.0\n\n" "Geodätische Vermessungsarbeiten mit JXL-Dateien\n\n" "Features:\n" - "• JXL-Datei Analyse mit TreeView\n" + "• JXL-Datei Analyse mit Berechnungsprotokoll\n" "• COR/CSV/TXT/DXF Export\n" "• Koordinatentransformation\n" - "• Georeferenzierung\n" - "• Netzausgleichung\n" + "• Georeferenzierung mit automatischer Punktzuordnung\n" + "• Netzausgleichung (korrektes Konzept)\n" + "• Datenfluss zwischen Modulen\n" "• Referenzpunkt-Anpassung") @@ -1741,7 +2044,7 @@ def main(): app = QApplication(sys.argv) app.setStyle("Fusion") - # Dunkleres Theme + # Theme palette = QPalette() palette.setColor(QPalette.Window, QColor(240, 240, 240)) palette.setColor(QPalette.WindowText, QColor(0, 0, 0)) diff --git a/modules/__pycache__/jxl_parser.cpython-311.pyc b/modules/__pycache__/jxl_parser.cpython-311.pyc index 77a2ba91ddfe4b168106640baa14e7c8999ebf97..e837344db3d808f7b56633620202fcfb668cc604 100644 GIT binary patch delta 19717 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 J|J3r`{~tM|QR4sr diff --git a/modules/jxl_parser.py b/modules/jxl_parser.py index 32b5a91..d136727 100644 --- a/modules/jxl_parser.py +++ b/modules/jxl_parser.py @@ -1,6 +1,7 @@ """ JXL Parser Module - Trimble JXL-Datei Parser Liest und analysiert Trimble JXL-Dateien (XML-basiert) +Version 3.0 - Überarbeitet für korrekte Stationierungserkennung """ import xml.etree.ElementTree as ET @@ -66,6 +67,10 @@ class Station: # Orientierung orientation_correction: Optional[float] = None + # Anzahl Messungen + num_backsight_measurements: int = 0 + num_backsight_points: int = 0 + record_id: str = "" timestamp: str = "" @@ -77,6 +82,7 @@ class Target: prism_constant: float = 0.0 target_height: float = 0.0 record_id: str = "" + timestamp: str = "" @dataclass @@ -124,6 +130,47 @@ class Line: start_station: float = 0.0 +@dataclass +class Measurement: + """Detaillierte Messung mit allen Rohdaten""" + point_name: str + station_id: str + station_name: str + target_id: str + + # Rohdaten + horizontal_circle: Optional[float] = None + vertical_circle: Optional[float] = None + edm_distance: Optional[float] = None + face: str = "Face1" + + # Berechnete Koordinaten + north: Optional[float] = None + east: Optional[float] = None + elevation: Optional[float] = None + + # Prismenkonstante + prism_constant: float = 0.0 + prism_type: str = "" + target_height: float = 0.0 + + # Klassifikation + classification: str = "" # BackSight, Normal + deleted: bool = False + + # Standardabweichungen + hz_std_error: Optional[float] = None + vz_std_error: Optional[float] = None + dist_std_error: Optional[float] = None + + # Atmosphäre + pressure: Optional[float] = None + temperature: Optional[float] = None + + timestamp: str = "" + record_id: str = "" + + class JXLParser: """Parser für Trimble JXL-Dateien""" @@ -146,6 +193,12 @@ class JXLParser: # Alle Messungen (auch gelöschte) self.all_point_records: List[Point] = [] + # Detaillierte Messungen für Protokoll + self.measurements: List[Measurement] = [] + + # Station zu Messungen Mapping + self.station_measurements: Dict[str, List[Measurement]] = {} + self.raw_xml = None self.file_path: str = "" @@ -171,10 +224,15 @@ class JXLParser: # Stationskoordinaten aus Punkten zuweisen self._assign_station_coordinates() + # Detaillierte Messungen erstellen + self._create_detailed_measurements() + return True except Exception as e: print(f"Fehler beim Parsen: {e}") + import traceback + traceback.print_exc() return False def _parse_element(self, element): @@ -192,7 +250,7 @@ class JXLParser: elif tag == 'StationRecord': self._parse_station(element, record_id, timestamp) elif tag == 'TargetRecord': - self._parse_target(element, record_id) + self._parse_target(element, record_id, timestamp) elif tag == 'BackBearingRecord': self._parse_backbearing(element, record_id) elif tag == 'InstrumentRecord': @@ -385,12 +443,21 @@ class JXLParser: if ori_elem is not None and ori_elem.text: station.orientation_correction = float(ori_elem.text) + num_bs_meas = element.find('NumberOfBacksightMeasurements') + if num_bs_meas is not None and num_bs_meas.text: + station.num_backsight_measurements = int(num_bs_meas.text) + + num_bs_pts = element.find('NumberOfBacksightPoints') + if num_bs_pts is not None and num_bs_pts.text: + station.num_backsight_points = int(num_bs_pts.text) + self.stations[record_id] = station - def _parse_target(self, element, record_id: str): + def _parse_target(self, element, record_id: str, timestamp: str = ""): """Parst ein Target/Prisma""" target = Target() target.record_id = record_id + target.timestamp = timestamp type_elem = element.find('PrismType') if type_elem is not None and type_elem.text: @@ -531,6 +598,58 @@ class JXLParser: station.east = point.east station.elevation = point.elevation + def _create_detailed_measurements(self): + """Erstellt detaillierte Messungen für das Berechnungsprotokoll""" + self.measurements = [] + self.station_measurements = {} + + for point in self.all_point_records: + if not point.station_id: + continue + + # Station finden + station = self.stations.get(point.station_id) + station_name = station.name if station else "?" + + # Target/Prismenkonstante finden + target = self.targets.get(point.target_id) + prism_const = target.prism_constant if target else 0.0 + prism_type = target.prism_type if target else "" + target_height = target.target_height if target else 0.0 + + meas = Measurement( + point_name=point.name, + station_id=point.station_id, + station_name=station_name, + target_id=point.target_id, + horizontal_circle=point.horizontal_circle, + vertical_circle=point.vertical_circle, + edm_distance=point.edm_distance, + face=point.face, + north=point.north, + east=point.east, + elevation=point.elevation, + prism_constant=prism_const, + prism_type=prism_type, + target_height=target_height, + classification=point.classification, + deleted=point.deleted, + hz_std_error=point.hz_std_error, + vz_std_error=point.vz_std_error, + dist_std_error=point.dist_std_error, + pressure=point.pressure, + temperature=point.temperature, + timestamp=point.timestamp, + record_id=point.record_id + ) + + self.measurements.append(meas) + + # Station-Mapping + if point.station_id not in self.station_measurements: + self.station_measurements[point.station_id] = [] + self.station_measurements[point.station_id].append(meas) + def get_active_points(self) -> Dict[str, Point]: """Gibt nur aktive (nicht gelöschte) Punkte zurück""" return {name: p for name, p in self.points.items() if not p.deleted} @@ -549,10 +668,25 @@ class JXLParser: return [p for p in self.all_point_records if p.station_id == station_id and not p.deleted] + def get_detailed_measurements_from_station(self, station_id: str) -> List[Measurement]: + """Gibt detaillierte Messungen von einer Station zurück""" + return self.station_measurements.get(station_id, []) + def get_prism_constants(self) -> Dict[str, float]: """Gibt alle verwendeten Prismenkonstanten zurück""" return {tid: t.prism_constant for tid, t in self.targets.items()} + def get_unique_prism_types(self) -> List[Tuple[str, str, float]]: + """Gibt eindeutige Prismentypen mit Konstanten zurück: (ID, Type, Constant)""" + seen = set() + result = [] + for tid, target in self.targets.items(): + key = (target.prism_type, target.prism_constant) + if key not in seen: + seen.add(key) + result.append((tid, target.prism_type, target.prism_constant)) + return result + def modify_prism_constant(self, target_id: str, new_constant: float): """Ändert die Prismenkonstante für ein Target""" if target_id in self.targets: @@ -576,6 +710,62 @@ class JXLParser: return list(self.lines.values())[0] return None + def get_reference_points(self) -> List[str]: + """ + Gibt die echten Passpunkte zurück. + Das sind Punkte mit bekannten Koordinaten, die zur Orientierung verwendet werden. + WICHTIG: Standpunkte (1001, 1002 etc.) sind KEINE Festpunkte! + + Festpunkte sind: + - Punkte aus Referenzlinien (5001, 5002) + - Punkte mit Method="Coordinates" oder "AzimuthOnly" (und NICHT als Station verwendet) + """ + ref_points = set() + + # Alle Stationsnamen sammeln (diese sind KEINE Festpunkte) + station_names = set(s.name for s in self.stations.values() if s.name) + + # Punkte aus Referenzlinien - das sind die echten Passpunkte + for line in self.lines.values(): + if line.start_point and line.start_point not in station_names: + ref_points.add(line.start_point) + if line.end_point and line.end_point not in station_names: + ref_points.add(line.end_point) + + # Punkte mit Method="Coordinates" (aber nicht Stationen) + for name, point in self.points.items(): + if name in station_names: + continue # Stationen überspringen + if point.method == 'Coordinates': + ref_points.add(name) + elif point.method == 'AzimuthOnly': + ref_points.add(name) + + return list(ref_points) + + def get_station_points(self) -> List[str]: + """ + Gibt Standpunkte zurück (1000er, 2000er Serie, etc.) + Das sind Punkte, an denen das Instrument aufgestellt wurde. + Gibt eindeutige Namen zurück. + """ + return list(set(station.name for station in self.stations.values() if station.name)) + + def get_measurement_points(self) -> List[str]: + """ + Gibt reine Messpunkte zurück (3000er Serie, etc.) + Das sind Punkte, die weder Passpunkte noch Standpunkte sind. + """ + ref_points = set(self.get_reference_points()) + station_points = set(self.get_station_points()) + + measurement_points = [] + for name, point in self.get_active_points().items(): + if name not in ref_points and name not in station_points: + measurement_points.append(name) + + return measurement_points + def gon_to_rad(self, gon: float) -> float: """Konvertiert Gon zu Radiant""" return gon * math.pi / 200.0 @@ -592,24 +782,167 @@ class JXLParser: summary.append(f"Zone: {self.zone_name}") summary.append(f"Datum: {self.datum_name}") summary.append(f"Winkeleinheit: {self.angle_units}") - summary.append(f"") + summary.append("") summary.append(f"Anzahl Punkte (aktiv): {len(self.get_active_points())}") summary.append(f"Anzahl Stationen: {len(self.stations)}") summary.append(f"Anzahl Messungen gesamt: {len(self.all_point_records)}") summary.append(f"Anzahl Targets/Prismen: {len(self.targets)}") summary.append(f"Anzahl Referenzlinien: {len(self.lines)}") - # Stationsübersicht - summary.append(f"\nStationen:") - for sid, station in self.stations.items(): - summary.append(f" - {station.name}: {station.station_type}") + return "\n".join(summary) + + def get_calculation_protocol(self) -> str: + """ + Erstellt ein detailliertes Berechnungsprotokoll mit allen Rohdaten + """ + lines = [] + lines.append("=" * 80) + lines.append("BERECHNUNGSPROTOKOLL") + lines.append("=" * 80) + lines.append(f"Job: {self.job_name}") + lines.append(f"Datei: {self.file_path}") + lines.append("") + + # Koordinatensystem + lines.append("-" * 80) + lines.append("KOORDINATENSYSTEM") + lines.append("-" * 80) + lines.append(f"System: {self.coordinate_system}") + lines.append(f"Zone: {self.zone_name}") + lines.append(f"Datum: {self.datum_name}") + lines.append(f"Winkeleinheit: {self.angle_units}") + lines.append(f"Distanzeinheit: {self.distance_units}") + lines.append("") + + # Instrumente + lines.append("-" * 80) + lines.append("INSTRUMENTE") + lines.append("-" * 80) + for inst_id, inst in self.instruments.items(): + if inst.model: + lines.append(f" {inst.model} (SN: {inst.serial})") + lines.append(f" Typ: {inst.instrument_type}") + lines.append(f" EDM-Präzision: {inst.edm_precision*1000:.1f} mm + {inst.edm_ppm} ppm") + lines.append(f" Winkel-Präzision: {inst.hz_precision*1000:.3f} mgon") + lines.append("") + + # Atmosphäre + lines.append("-" * 80) + lines.append("ATMOSPHÄRISCHE BEDINGUNGEN") + lines.append("-" * 80) + for atm_id, atm in self.atmospheres.items(): + lines.append(f" Druck: {atm.pressure:.1f} hPa, Temperatur: {atm.temperature:.1f} °C") + lines.append(f" PPM: {atm.ppm:.2f}, Refraktionskoeff.: {atm.refraction_coefficient:.3f}") + lines.append("") # Prismenkonstanten - summary.append(f"\nPrismenkonstanten:") + lines.append("-" * 80) + lines.append("PRISMENKONSTANTEN") + lines.append("-" * 80) + seen = set() for tid, target in self.targets.items(): - summary.append(f" - {target.prism_type}: {target.prism_constant*1000:.1f} mm") + key = (target.prism_type, target.prism_constant) + if key not in seen: + seen.add(key) + lines.append(f" {target.prism_type}: {target.prism_constant*1000:+.1f} mm") + lines.append("") - return "\n".join(summary) + # Referenzlinien + if self.lines: + lines.append("-" * 80) + lines.append("REFERENZLINIEN") + lines.append("-" * 80) + for name, line in self.lines.items(): + lines.append(f" {name}: {line.start_point} → {line.end_point}") + lines.append("") + + # Stationierungen + lines.append("=" * 80) + lines.append("STATIONIERUNGEN UND MESSUNGEN") + lines.append("=" * 80) + + for station_id, station in sorted(self.stations.items(), key=lambda x: x[1].timestamp): + lines.append("") + lines.append("-" * 80) + lines.append(f"STATION: {station.name}") + lines.append("-" * 80) + lines.append(f" Typ: {station.station_type}") + lines.append(f" Instrumentenhöhe: {station.theodolite_height:.4f} m") + lines.append(f" Maßstab: {station.scale_factor:.8f}") + + if station.east is not None: + lines.append(f" Koordinaten: E={station.east:.4f}, N={station.north:.4f}, H={station.elevation or 0:.4f}") + + # Backbearing finden + for bb_id, bb in self.backbearings.items(): + if bb.station_record_id == station_id: + lines.append(f" Orientierung:") + lines.append(f" Anschlusspunkt: {bb.backsight}") + if bb.face1_hz is not None: + lines.append(f" Hz-Kreis (L1): {bb.face1_hz:.6f} gon") + if bb.face2_hz is not None: + lines.append(f" Hz-Kreis (L2): {bb.face2_hz:.6f} gon") + if bb.orientation_correction is not None: + lines.append(f" Orientierungskorrektur: {bb.orientation_correction:.6f} gon") + + # Messungen + measurements = self.get_detailed_measurements_from_station(station_id) + + # Anschlussmessungen + backsight_meas = [m for m in measurements if m.classification == 'BackSight' and not m.deleted] + if backsight_meas: + lines.append("") + lines.append(" ANSCHLUSSMESSUNGEN:") + for m in backsight_meas: + lines.append(f" Punkt: {m.point_name}") + if m.horizontal_circle is not None: + lines.append(f" Hz: {m.horizontal_circle:.6f} gon") + if m.vertical_circle is not None: + lines.append(f" V: {m.vertical_circle:.6f} gon") + if m.edm_distance is not None: + lines.append(f" D: {m.edm_distance:.4f} m (Prismenkonstante: {m.prism_constant*1000:+.1f} mm)") + if m.east is not None: + lines.append(f" → E={m.east:.4f}, N={m.north:.4f}, H={m.elevation or 0:.4f}") + + # Normale Messungen + normal_meas = [m for m in measurements if m.classification != 'BackSight' and not m.deleted] + if normal_meas: + lines.append("") + lines.append(" MESSUNGEN:") + lines.append(f" {'Punkt':<10} {'Hz [gon]':>14} {'V [gon]':>14} {'D [m]':>12} {'PK [mm]':>10} {'E':>12} {'N':>12} {'H':>10}") + lines.append(" " + "-" * 96) + + for m in normal_meas: + hz = f"{m.horizontal_circle:.6f}" if m.horizontal_circle is not None else "-" + v = f"{m.vertical_circle:.6f}" if m.vertical_circle is not None else "-" + d = f"{m.edm_distance:.4f}" if m.edm_distance is not None else "-" + pk = f"{m.prism_constant*1000:+.1f}" + e = f"{m.east:.4f}" if m.east is not None else "-" + n = f"{m.north:.4f}" if m.north is not None else "-" + h = f"{m.elevation:.4f}" if m.elevation is not None else "-" + + lines.append(f" {m.point_name:<10} {hz:>14} {v:>14} {d:>12} {pk:>10} {e:>12} {n:>12} {h:>10}") + + # Alle berechneten Punkte + lines.append("") + lines.append("=" * 80) + lines.append("BERECHNETE KOORDINATEN") + lines.append("=" * 80) + lines.append(f"{'Punkt':<12} {'East [m]':>14} {'North [m]':>14} {'Elev [m]':>12} {'Methode':<20}") + lines.append("-" * 80) + + for name, point in sorted(self.get_active_points().items()): + e = f"{point.east:.4f}" if point.east is not None else "-" + n = f"{point.north:.4f}" if point.north is not None else "-" + h = f"{point.elevation:.4f}" if point.elevation is not None else "-" + lines.append(f"{name:<12} {e:>14} {n:>14} {h:>12} {point.method:<20}") + + lines.append("") + lines.append("=" * 80) + lines.append(f"Protokoll erstellt") + lines.append("=" * 80) + + return "\n".join(lines) def create_copy(self): """Erstellt eine tiefe Kopie des Parsers""" diff --git a/test_data/berechnungsprotokoll_test.txt b/test_data/berechnungsprotokoll_test.txt new file mode 100644 index 0000000..d7d1c30 --- /dev/null +++ b/test_data/berechnungsprotokoll_test.txt @@ -0,0 +1,765 @@ +================================================================================ +BERECHNUNGSPROTOKOLL +================================================================================ +Job: Baumschulenstr_93 +Datei: test_data/Baumschulenstr_93.jxl + +-------------------------------------------------------------------------------- +KOORDINATENSYSTEM +-------------------------------------------------------------------------------- +System: Germany +Zone: ETRS89_UTM32 +Datum: ETRS89 +Winkeleinheit: Gons +Distanzeinheit: Metres + +-------------------------------------------------------------------------------- +INSTRUMENTE +-------------------------------------------------------------------------------- + SX12 (SN: 30710160) + Typ: TrimbleSX10 + EDM-Präzision: 1.0 mm + 1.5 ppm + Winkel-Präzision: 0.278 mgon + SX12 (SN: 30710160) + Typ: TrimbleSX10 + EDM-Präzision: 1.0 mm + 1.5 ppm + Winkel-Präzision: 0.278 mgon + SX12 (SN: 30710160) + Typ: TrimbleSX10 + EDM-Präzision: 1.0 mm + 1.5 ppm + Winkel-Präzision: 0.278 mgon + SX12 (SN: 30710160) + Typ: TrimbleSX10 + EDM-Präzision: 1.0 mm + 1.5 ppm + Winkel-Präzision: 0.278 mgon + SX12 (SN: 30710160) + Typ: TrimbleSX10 + EDM-Präzision: 1.0 mm + 1.5 ppm + Winkel-Präzision: 0.278 mgon + SX12 (SN: 30710160) + Typ: TrimbleSX10 + EDM-Präzision: 1.0 mm + 1.5 ppm + Winkel-Präzision: 0.278 mgon + SX12 (SN: 30710160) + Typ: TrimbleSX10 + EDM-Präzision: 1.0 mm + 1.5 ppm + Winkel-Präzision: 0.278 mgon + SX12 (SN: 30710160) + Typ: TrimbleSX10 + EDM-Präzision: 1.0 mm + 1.5 ppm + Winkel-Präzision: 0.278 mgon + SX12 (SN: 30710160) + Typ: TrimbleSX10 + EDM-Präzision: 1.0 mm + 1.5 ppm + Winkel-Präzision: 0.278 mgon + SX12 (SN: 30710160) + Typ: TrimbleSX10 + EDM-Präzision: 1.0 mm + 1.5 ppm + Winkel-Präzision: 0.278 mgon + SX12 (SN: 30710160) + Typ: TrimbleSX10 + EDM-Präzision: 1.0 mm + 1.5 ppm + Winkel-Präzision: 0.278 mgon + SX12 (SN: 30710160) + Typ: TrimbleSX10 + EDM-Präzision: 1.0 mm + 1.5 ppm + Winkel-Präzision: 0.278 mgon + SX12 (SN: 30710160) + Typ: TrimbleSX10 + EDM-Präzision: 1.0 mm + 1.5 ppm + Winkel-Präzision: 0.278 mgon + SX12 (SN: 30710160) + Typ: TrimbleSX10 + EDM-Präzision: 1.0 mm + 1.5 ppm + Winkel-Präzision: 0.278 mgon + SX12 (SN: 30710160) + Typ: TrimbleSX10 + EDM-Präzision: 1.0 mm + 1.5 ppm + Winkel-Präzision: 0.278 mgon + SX12 (SN: 30710160) + Typ: TrimbleSX10 + EDM-Präzision: 1.0 mm + 1.5 ppm + Winkel-Präzision: 0.278 mgon + SX12 (SN: 30710160) + Typ: TrimbleSX10 + EDM-Präzision: 1.0 mm + 1.5 ppm + Winkel-Präzision: 0.278 mgon + SX12 (SN: 30710160) + Typ: TrimbleSX10 + EDM-Präzision: 1.0 mm + 1.5 ppm + Winkel-Präzision: 0.278 mgon + SX12 (SN: 30710160) + Typ: TrimbleSX10 + EDM-Präzision: 1.0 mm + 1.5 ppm + Winkel-Präzision: 0.278 mgon + SX12 (SN: 30710160) + Typ: TrimbleSX10 + EDM-Präzision: 1.0 mm + 1.5 ppm + Winkel-Präzision: 0.278 mgon + SX12 (SN: 30710160) + Typ: TrimbleSX10 + EDM-Präzision: 1.0 mm + 1.5 ppm + Winkel-Präzision: 0.278 mgon + SX12 (SN: 30710160) + Typ: TrimbleSX10 + EDM-Präzision: 1.0 mm + 1.5 ppm + Winkel-Präzision: 0.278 mgon + SX12 (SN: 30710160) + Typ: TrimbleSX10 + EDM-Präzision: 1.0 mm + 1.5 ppm + Winkel-Präzision: 0.278 mgon + SX12 (SN: 30710160) + Typ: TrimbleSX10 + EDM-Präzision: 1.0 mm + 1.5 ppm + Winkel-Präzision: 0.278 mgon + SX12 (SN: 30710160) + Typ: TrimbleSX10 + EDM-Präzision: 1.0 mm + 1.5 ppm + Winkel-Präzision: 0.278 mgon + SX12 (SN: 30710160) + Typ: TrimbleSX10 + EDM-Präzision: 1.0 mm + 1.5 ppm + Winkel-Präzision: 0.278 mgon + +-------------------------------------------------------------------------------- +ATMOSPHÄRISCHE BEDINGUNGEN +-------------------------------------------------------------------------------- + Druck: 1012.3 hPa, Temperatur: 2.0 °C + PPM: -17.39, Refraktionskoeff.: 0.142 + Druck: 1012.7 hPa, Temperatur: 2.0 °C + PPM: -17.50, Refraktionskoeff.: 0.142 + Druck: 1012.5 hPa, Temperatur: 2.0 °C + PPM: -17.44, Refraktionskoeff.: 0.142 + Druck: 1012.4 hPa, Temperatur: 2.0 °C + PPM: -17.42, Refraktionskoeff.: 0.142 + Druck: 1012.4 hPa, Temperatur: 2.0 °C + PPM: -17.42, Refraktionskoeff.: 0.142 + Druck: 1012.4 hPa, Temperatur: 2.0 °C + PPM: -17.42, Refraktionskoeff.: 0.142 + Druck: 1012.4 hPa, Temperatur: 2.0 °C + PPM: -17.42, Refraktionskoeff.: 0.142 + Druck: 1012.3 hPa, Temperatur: 2.0 °C + PPM: -17.39, Refraktionskoeff.: 0.142 + Druck: 1012.3 hPa, Temperatur: 2.0 °C + PPM: -17.39, Refraktionskoeff.: 0.142 + Druck: 1012.2 hPa, Temperatur: 2.0 °C + PPM: -17.36, Refraktionskoeff.: 0.142 + Druck: 1011.8 hPa, Temperatur: 6.0 °C + PPM: -13.13, Refraktionskoeff.: 0.142 + Druck: 1011.7 hPa, Temperatur: 6.0 °C + PPM: -13.10, Refraktionskoeff.: 0.142 + Druck: 1011.8 hPa, Temperatur: 6.0 °C + PPM: -13.13, Refraktionskoeff.: 0.142 + Druck: 1011.7 hPa, Temperatur: 6.0 °C + PPM: -13.10, Refraktionskoeff.: 0.142 + Druck: 1011.9 hPa, Temperatur: 6.0 °C + PPM: -13.16, Refraktionskoeff.: 0.142 + Druck: 1011.7 hPa, Temperatur: 6.0 °C + PPM: -13.10, Refraktionskoeff.: 0.142 + Druck: 1011.6 hPa, Temperatur: 6.0 °C + PPM: -13.07, Refraktionskoeff.: 0.142 + Druck: 1011.6 hPa, Temperatur: 6.0 °C + PPM: -13.07, Refraktionskoeff.: 0.142 + Druck: 1011.6 hPa, Temperatur: 6.0 °C + PPM: -13.07, Refraktionskoeff.: 0.142 + Druck: 1013.5 hPa, Temperatur: 2.0 °C + PPM: -17.73, Refraktionskoeff.: 0.142 + Druck: 1013.6 hPa, Temperatur: 2.0 °C + PPM: -17.76, Refraktionskoeff.: 0.142 + Druck: 1014.4 hPa, Temperatur: 2.0 °C + PPM: -17.98, Refraktionskoeff.: 0.142 + Druck: 1015.1 hPa, Temperatur: 3.0 °C + PPM: -17.14, Refraktionskoeff.: 0.142 + +-------------------------------------------------------------------------------- +PRISMENKONSTANTEN +-------------------------------------------------------------------------------- + DRTarget: +0.0 mm + CustomPrism: -34.4 mm + SSeries360Prism: +2.0 mm + +-------------------------------------------------------------------------------- +REFERENZLINIEN +-------------------------------------------------------------------------------- + 5001-5002: 5001 → 5002 + +================================================================================ +STATIONIERUNGEN UND MESSUNGEN +================================================================================ + +-------------------------------------------------------------------------------- +STATION: 1001 +-------------------------------------------------------------------------------- + Typ: ReflineStationSetup + Instrumentenhöhe: 0.0000 m + Maßstab: 1.00000000 + Koordinaten: E=22.7880, N=13.5648, H=-1.9522 + Orientierung: + Anschlusspunkt: 5001 + Hz-Kreis (L1): 239.236244 gon + Orientierungskorrektur: -0.000000 gon + + ANSCHLUSSMESSUNGEN: + Punkt: 5001 + Hz: 239.236244 gon + V: 85.789957 gon + D: 26.5920 m (Prismenkonstante: +0.0 mm) + → E=0.0000, N=-0.0000, H=0.0000 + Punkt: 5002 + Hz: 264.591917 gon + V: 85.212801 gon + D: 22.9704 m (Prismenkonstante: +0.0 mm) + → E=0.0000, N=11.4075, H=-0.0352 + + MESSUNGEN: + Punkt Hz [gon] V [gon] D [m] PK [mm] E N H + ------------------------------------------------------------------------------------------------ + 2001 265.436604 88.516654 22.8548 +0.0 0.0137 11.7471 -1.3606 + 2002 351.584401 89.059438 17.5917 +0.0 20.2138 30.9645 -1.6634 + 2003 92.434873 93.453802 11.0245 +0.0 33.7824 13.0973 -2.6163 + 2004 184.530811 88.986816 29.0897 +0.0 20.4904 -15.4289 -1.4378 + 3001 91.788959 93.222299 11.1076 -34.4 33.8381 13.2197 -2.5746 + 3002 11.990548 89.833351 16.9801 -34.4 26.3084 30.1404 -1.9029 + 3003 168.045338 90.540987 17.2672 -34.4 26.3573 -3.2932 -2.1149 + 3004 238.208180 92.287471 26.8102 -34.4 0.0479 -0.5301 -3.0209 + 3005 253.466021 93.513311 23.8905 -34.4 -0.0383 6.7887 -3.4141 + 6001 270.486529 90.313536 40.9848 +2.0 -18.1960 13.9129 -2.1764 + 6002 268.633987 90.496928 38.4763 +2.0 -15.6773 12.6476 -2.2858 + 6003 268.815519 90.375034 49.1165 +2.0 -26.3181 12.5495 -2.2735 + 6004 270.337427 89.526485 47.5121 +2.0 -24.7228 13.8446 -1.5594 + 3006 268.849564 80.035382 21.9763 -34.4 1.1818 13.1309 1.8446 + 3007 278.253596 80.281424 22.5075 -34.4 0.8672 16.7446 1.8414 + +-------------------------------------------------------------------------------- +STATION: 1002 +-------------------------------------------------------------------------------- + Typ: StandardResection + Instrumentenhöhe: 0.0000 m + Maßstab: 1.00000000 + Koordinaten: E=-20.8826, N=13.0076, H=-1.7418 + Orientierung: + Anschlusspunkt: + +-------------------------------------------------------------------------------- +STATION: 1002 +-------------------------------------------------------------------------------- + Typ: StandardResection + Instrumentenhöhe: 0.0000 m + Maßstab: 1.00000000 + Koordinaten: E=-20.8826, N=13.0076, H=-1.7418 + Orientierung: + Anschlusspunkt: + +-------------------------------------------------------------------------------- +STATION: 1002 +-------------------------------------------------------------------------------- + Typ: StandardResection + Instrumentenhöhe: 0.0000 m + Maßstab: 1.00000000 + Koordinaten: E=-20.8826, N=13.0076, H=-1.7418 + Orientierung: + Anschlusspunkt: + +-------------------------------------------------------------------------------- +STATION: 1002 +-------------------------------------------------------------------------------- + Typ: StandardResection + Instrumentenhöhe: 0.0000 m + Maßstab: 1.00000000 + Koordinaten: E=-20.8826, N=13.0076, H=-1.7418 + Orientierung: + Anschlusspunkt: + +-------------------------------------------------------------------------------- +STATION: 1002 +-------------------------------------------------------------------------------- + Typ: StandardResection + Instrumentenhöhe: 0.0000 m + Maßstab: 1.00000000 + Koordinaten: E=-20.8826, N=13.0076, H=-1.7418 + Orientierung: + Anschlusspunkt: + +-------------------------------------------------------------------------------- +STATION: 1002 +-------------------------------------------------------------------------------- + Typ: StandardResection + Instrumentenhöhe: 0.0000 m + Maßstab: 1.00000000 + Koordinaten: E=-20.8826, N=13.0076, H=-1.7418 + Orientierung: + Anschlusspunkt: + +-------------------------------------------------------------------------------- +STATION: 1001 +-------------------------------------------------------------------------------- + Typ: StandardResection + Instrumentenhöhe: 0.0000 m + Maßstab: 1.00000000 + Koordinaten: E=22.7880, N=13.5648, H=-1.9522 + Orientierung: + Anschlusspunkt: + +-------------------------------------------------------------------------------- +STATION: 1002 +-------------------------------------------------------------------------------- + Typ: StandardResection + Instrumentenhöhe: 0.0000 m + Maßstab: 1.00000000 + Koordinaten: E=-20.8826, N=13.0076, H=-1.7418 + Orientierung: + Anschlusspunkt: 6002 + Hz-Kreis (L1): 93.956217 gon + Orientierungskorrektur: 0.000000 gon + + ANSCHLUSSMESSUNGEN: + Punkt: 6002 + Hz: 93.956631 gon + V: 95.946279 gon + D: 5.2442 m (Prismenkonstante: +2.0 mm) + → E=-15.6772, N=12.6476, H=-2.2853 + Punkt: 6001 + Hz: 71.377052 gon + V: 98.724150 gon + D: 2.8662 m (Prismenkonstante: +2.0 mm) + → E=-18.1961, N=13.9129, H=-2.1769 + + MESSUNGEN: + Punkt Hz [gon] V [gon] D [m] PK [mm] E N H + ------------------------------------------------------------------------------------------------ + 6004 282.315481 87.360125 3.9370 +2.0 -24.7269 13.8469 -1.5604 + 6003 265.248250 95.575732 5.4814 +2.0 -26.3212 12.5555 -2.2746 + 2005 55.324054 89.930544 9.4179 +0.0 -13.1377 18.3656 -1.7304 + 2006 155.116934 88.419445 2.6637 +0.0 -19.7623 10.5922 -1.6684 + 2007 284.456560 88.475945 9.2002 +0.0 -29.7882 15.3035 -1.4972 + 2008 2.836596 90.344968 9.3302 +0.0 -20.4209 22.3261 -1.7980 + 3008 104.036919 79.578164 8.1132 -34.4 -13.1745 11.0805 -0.2805 + 3009 106.271269 55.493144 9.7865 -34.4 -13.1684 10.7560 3.7827 + 3010 141.264009 71.879009 11.5367 -34.4 -14.0423 4.4805 1.8356 + 3011 84.212803 46.227865 10.7756 -34.4 -13.1661 13.7897 5.6887 + 3012 103.241791 40.453887 12.2564 -34.4 -13.1636 11.1912 7.5580 + 3013 107.313392 32.554379 15.0589 -34.4 -13.1644 10.6017 10.9218 + 2009 93.004786 91.128691 7.7513 +0.0 -13.1436 12.6014 -1.8945 + 7002 89.208170 98.211357 13.5359 +0.0 -7.4870 13.1927 -3.6751 + 7001 48.986594 82.685411 10.3531 +0.0 -13.1343 19.7463 -0.4237 + 3014 90.215335 100.063679 10.6263 -34.4 -10.4540 12.9684 -3.5927 + 3015 85.047972 96.965032 15.3344 -34.4 -5.7525 14.3185 -3.5971 + 3016 90.506741 95.662644 18.8422 -34.4 -2.1676 12.8421 -3.5976 + 3017 128.641072 85.046657 5.0761 -34.4 -16.9595 9.8712 -1.3065 + 3018 121.542651 89.814930 9.1162 -34.4 -13.1429 8.2567 -1.7125 + 3019 181.651710 85.735400 8.5887 -34.4 -21.1285 4.4806 -1.1057 + 3020 249.998353 92.016901 11.0188 -34.4 -31.1979 9.2528 -2.1284 + 3021 294.162896 92.650905 9.6096 -34.4 -29.6094 16.9228 -2.1847 + 3022 313.819047 97.218087 12.9748 -34.4 -30.1454 21.8961 -3.3677 + 3023 0.216398 105.393377 6.1904 -34.4 -20.8602 18.9426 -3.3759 + 6005 328.887378 93.120336 9.2044 +2.0 -25.6326 20.8779 -2.2430 + +-------------------------------------------------------------------------------- +STATION: 1003 +-------------------------------------------------------------------------------- + Typ: StandardResection + Instrumentenhöhe: 0.0000 m + Maßstab: 1.00000000 + Koordinaten: E=23.0704, N=14.0268, H=-1.9693 + Orientierung: + Anschlusspunkt: 2003 + Hz-Kreis (L1): 94.959326 gon + Orientierungskorrektur: 0.000000 gon + + ANSCHLUSSMESSUNGEN: + Punkt: 2003 + Hz: 94.959794 gon + V: 93.444510 gon + D: 10.7719 m (Prismenkonstante: +0.0 mm) + → E=33.7823, N=13.0972, H=-2.6165 + Punkt: 2004 + Hz: 185.005488 gon + V: 88.969885 gon + D: 29.5737 m (Prismenkonstante: +0.0 mm) + → E=20.4905, N=-15.4288, H=-1.4376 + + MESSUNGEN: + Punkt Hz [gon] V [gon] D [m] PK [mm] E N H + ------------------------------------------------------------------------------------------------ + 2001 264.353319 88.495229 23.1772 +0.0 0.0139 11.7472 -1.3607 + 2002 350.428306 88.979489 17.1793 +0.0 20.2143 30.9640 -1.6634 + 3024 266.931582 70.929310 23.2213 -34.4 1.1879 12.8538 5.6065 + 3025 245.321058 72.500049 25.2564 -34.4 1.2132 3.9834 5.6149 + 3026 278.310255 64.079301 25.9902 -34.4 -0.0288 17.4009 9.3765 + 3027 258.776027 57.318373 27.9772 -34.4 0.0016 9.4491 13.1187 + 3028 276.496934 56.966420 28.8298 -34.4 -0.9148 16.7583 13.7277 + 3029 244.136925 63.974174 27.2106 -34.4 1.0963 3.3743 9.9548 + +-------------------------------------------------------------------------------- +STATION: 1004 +-------------------------------------------------------------------------------- + Typ: StandardResection + Instrumentenhöhe: 0.0000 m + Maßstab: 1.00000000 + Koordinaten: E=-22.6960, N=19.7653, H=-1.9656 + Orientierung: + Anschlusspunkt: + +-------------------------------------------------------------------------------- +STATION: 1004 +-------------------------------------------------------------------------------- + Typ: StandardResection + Instrumentenhöhe: 0.0000 m + Maßstab: 1.00000000 + Koordinaten: E=-22.6960, N=19.7653, H=-1.9656 + Orientierung: + Anschlusspunkt: + +-------------------------------------------------------------------------------- +STATION: 1004 +-------------------------------------------------------------------------------- + Typ: StandardResection + Instrumentenhöhe: 0.0000 m + Maßstab: 1.00000000 + Koordinaten: E=-22.6960, N=19.7653, H=-1.9656 + Orientierung: + Anschlusspunkt: + +-------------------------------------------------------------------------------- +STATION: 1004 +-------------------------------------------------------------------------------- + Typ: StandardResection + Instrumentenhöhe: 0.0000 m + Maßstab: 1.00000000 + Koordinaten: E=-22.6960, N=19.7653, H=-1.9656 + Orientierung: + Anschlusspunkt: 6003 + Hz-Kreis (L1): 206.655476 gon + Orientierungskorrektur: 0.000000 gon + + ANSCHLUSSMESSUNGEN: + Punkt: 6003 + Hz: 206.657696 gon + V: 92.187503 gon + D: 8.0781 m (Prismenkonstante: +2.0 mm) + → E=-26.3185, N=12.5495, H=-2.2740 + Punkt: 6002 + Hz: 135.399836 gon + V: 91.831919 gon + D: 9.9997 m (Prismenkonstante: +2.0 mm) + → E=-15.6769, N=12.6476, H=-2.2853 + + MESSUNGEN: + Punkt Hz [gon] V [gon] D [m] PK [mm] E N H + ------------------------------------------------------------------------------------------------ + 2008 41.618403 87.175760 3.4242 +0.0 -20.4245 22.3221 -1.7969 + 6005 290.628459 95.011142 3.1504 +2.0 -25.6350 20.8717 -2.2410 + 6006 281.200295 90.279540 25.9497 +2.0 -48.1527 24.8060 -2.0922 + 6007 278.204159 90.037385 26.1203 +2.0 -48.5506 23.4929 -1.9826 + 6008 277.946838 90.365116 20.3991 +2.0 -42.9005 22.5858 -2.0956 + +-------------------------------------------------------------------------------- +STATION: 1005 +-------------------------------------------------------------------------------- + Typ: StandardResection + Instrumentenhöhe: 0.0000 m + Maßstab: 1.00000000 + Koordinaten: E=-46.0315, N=23.5752, H=-1.9509 + Orientierung: + Anschlusspunkt: + +-------------------------------------------------------------------------------- +STATION: 1005 +-------------------------------------------------------------------------------- + Typ: StandardResection + Instrumentenhöhe: 0.0000 m + Maßstab: 1.00000000 + Koordinaten: E=-46.0315, N=23.5752, H=-1.9509 + Orientierung: + Anschlusspunkt: + +-------------------------------------------------------------------------------- +STATION: 1005 +-------------------------------------------------------------------------------- + Typ: StandardResection + Instrumentenhöhe: 0.0000 m + Maßstab: 1.00000000 + Koordinaten: E=-46.0315, N=23.5752, H=-1.9509 + Orientierung: + Anschlusspunkt: + +-------------------------------------------------------------------------------- +STATION: 1005 +-------------------------------------------------------------------------------- + Typ: StandardResection + Instrumentenhöhe: 0.0000 m + Maßstab: 1.00000000 + Koordinaten: E=-46.0315, N=23.5752, H=-1.9509 + Orientierung: + Anschlusspunkt: + +-------------------------------------------------------------------------------- +STATION: 1005 +-------------------------------------------------------------------------------- + Typ: StandardResection + Instrumentenhöhe: 0.0000 m + Maßstab: 1.00000000 + Koordinaten: E=-46.0315, N=23.5752, H=-1.9509 + Orientierung: + Anschlusspunkt: 6008 + Hz-Kreis (L1): 107.537751 gon + Orientierungskorrektur: 0.000000 gon + + ANSCHLUSSMESSUNGEN: + Punkt: 6008 + Hz: 107.533353 gon + V: 92.515106 gon + D: 3.2862 m (Prismenkonstante: +2.0 mm) + → E=-42.8992, N=22.5856, H=-2.0952 + Punkt: 6005 + Hz: 97.532711 gon + V: 90.814217 gon + D: 20.5754 m (Prismenkonstante: +2.0 mm) + → E=-25.6340, N=20.8780, H=-2.2433 + + MESSUNGEN: + Punkt Hz [gon] V [gon] D [m] PK [mm] E N H + ------------------------------------------------------------------------------------------------ + 6006 300.079231 93.294893 2.4571 +2.0 -48.1559 24.8057 -2.0923 + 6007 268.029207 90.729397 2.5210 +2.0 -48.5528 23.4885 -1.9831 + 2010 72.458666 89.315065 5.4129 +0.0 -40.8707 25.2065 -1.8862 + 2011 156.471149 86.446196 8.2701 +0.0 -42.7364 16.0074 -1.4383 + 2012 200.203267 89.983377 14.4081 +0.0 -51.0073 10.0538 -1.9468 + 2012 200.203040 89.983277 14.4082 +0.0 -51.0073 10.0537 -1.9467 + 2012 - - - +0.0 -51.0073 10.0537 -1.9467 + 2013 348.886840 89.532943 2.9308 +0.0 -46.5964 26.4510 -1.9271 + 3030 123.407671 92.690617 5.3146 -34.4 -41.6286 20.6712 -2.1988 + 3031 150.550871 91.959334 7.5101 -34.4 -42.3583 17.0694 -2.2065 + 3032 171.423745 91.076213 14.2322 -34.4 -43.9146 9.5389 -2.2176 + 3033 95.228223 101.998035 7.2508 -34.4 -39.0022 22.9320 -3.4510 + +-------------------------------------------------------------------------------- +STATION: 1006 +-------------------------------------------------------------------------------- + Typ: StandardResection + Instrumentenhöhe: 0.0000 m + Maßstab: 1.00000000 + Koordinaten: E=22.9235, N=13.8783, H=-1.9035 + Orientierung: + Anschlusspunkt: 2003 + Hz-Kreis (L1): 94.113814 gon + Orientierungskorrektur: -0.000000 gon + + ANSCHLUSSMESSUNGEN: + Punkt: 2003 + Hz: 94.113477 gon + V: 93.746786 gon + D: 10.9105 m (Prismenkonstante: +0.0 mm) + → E=33.7824, N=13.0974, H=-2.6165 + Punkt: 2004 + Hz: 184.745778 gon + V: 89.092575 gon + D: 29.4123 m (Prismenkonstante: +0.0 mm) + → E=20.4904, N=-15.4290, H=-1.4377 + + MESSUNGEN: + Punkt Hz [gon] V [gon] D [m] PK [mm] E N H + ------------------------------------------------------------------------------------------------ + 2001 264.684949 88.648383 23.0157 +0.0 0.0135 11.7470 -1.3606 + 2002 350.986209 89.205135 17.3011 +0.0 20.2132 30.9638 -1.6635 + 3034 281.517975 53.972186 31.8128 -34.4 -2.2586 19.0099 16.7875 + 3035 264.965948 52.745699 31.5184 -34.4 -2.0392 11.6794 17.1551 + 3036 242.150573 56.648773 34.3234 -34.4 -2.4008 0.4984 16.9473 + +-------------------------------------------------------------------------------- +STATION: 1007 +-------------------------------------------------------------------------------- + Typ: StandardResection + Instrumentenhöhe: 0.0000 m + Maßstab: 1.00000000 + Koordinaten: E=-21.6340, N=18.9681, H=-1.8149 + Orientierung: + Anschlusspunkt: 2006 + Hz-Kreis (L1): 167.403390 gon + Orientierungskorrektur: 0.000000 gon + + ANSCHLUSSMESSUNGEN: + Punkt: 2006 + Hz: 167.408936 gon + V: 89.021905 gon + D: 8.5832 m (Prismenkonstante: +0.0 mm) + → E=-19.7633, N=10.5927, H=-1.6684 + Punkt: 2007 + Hz: 245.795284 gon + V: 87.963994 gon + D: 8.9448 m (Prismenkonstante: +0.0 mm) + → E=-29.7872, N=15.3031, H=-1.4971 + + MESSUNGEN: + Punkt Hz [gon] V [gon] D [m] PK [mm] E N H + ------------------------------------------------------------------------------------------------ + 2008 19.861431 89.726355 3.5698 +0.0 -20.4212 22.3255 -1.7979 + 2005 94.059110 89.430934 8.5181 +0.0 -13.1378 18.3652 -1.7303 + 3037 263.845826 68.234399 8.3395 -34.4 -29.3024 18.1412 1.2646 + 3038 237.574470 82.696322 10.3659 -34.4 -30.2838 13.4734 -0.5015 + 3039 239.173775 64.655784 11.1020 -34.4 -30.2231 13.8426 2.9225 + 3040 240.356419 50.325380 12.7866 -34.4 -30.1643 14.1136 6.3263 + 3041 238.337593 41.760336 15.2528 -34.4 -30.2609 13.6479 9.5368 + +-------------------------------------------------------------------------------- +STATION: 1008 +-------------------------------------------------------------------------------- + Typ: StandardResection + Instrumentenhöhe: 0.0000 m + Maßstab: 1.00000000 + Koordinaten: E=-46.5669, N=23.4502, H=-1.9004 + Orientierung: + Anschlusspunkt: 2012 + Hz-Kreis (L1): 198.338432 gon + Orientierungskorrektur: 0.000000 gon + + ANSCHLUSSMESSUNGEN: + Punkt: 2012 + Hz: 198.338354 gon + V: 90.188111 gon + D: 14.1139 m (Prismenkonstante: +0.0 mm) + → E=-51.0074, N=10.0534, H=-1.9468 + Punkt: 2013 + Hz: 359.438362 gon + V: 90.507602 gon + D: 3.0015 m (Prismenkonstante: +0.0 mm) + → E=-46.5963, N=26.4513, H=-1.9270 + + MESSUNGEN: + Punkt Hz [gon] V [gon] D [m] PK [mm] E N H + ------------------------------------------------------------------------------------------------ + 2010 72.862313 89.864020 5.9607 +0.0 -40.8710 25.2065 -1.8863 + 2011 152.768282 86.837999 8.3845 +0.0 -42.7361 16.0064 -1.4380 + 3042 166.156719 62.148888 14.0756 -34.4 -43.5965 11.3962 4.6592 + 3043 86.632675 40.256544 8.6348 -34.4 -41.0189 23.7766 4.6629 + +-------------------------------------------------------------------------------- +STATION: 1009 +-------------------------------------------------------------------------------- + Typ: StandardResection + Instrumentenhöhe: 0.0000 m + Maßstab: 1.00000000 + Koordinaten: E=-17.1322, N=16.5482, H=-1.9002 + Orientierung: + Anschlusspunkt: 2005 + Hz-Kreis (L1): 65.535513 gon + Orientierungskorrektur: 0.000000 gon + + ANSCHLUSSMESSUNGEN: + Punkt: 2005 + Hz: 65.536867 gon + V: 87.784398 gon + D: 4.3917 m (Prismenkonstante: +0.0 mm) + → E=-13.1379, N=18.3655, H=-1.7304 + Punkt: 2006 + Hz: 203.824812 gon + V: 87.961169 gon + D: 6.5149 m (Prismenkonstante: +0.0 mm) + → E=-19.7621, N=10.5924, H=-1.6684 + + MESSUNGEN: + Punkt Hz [gon] V [gon] D [m] PK [mm] E N H + ------------------------------------------------------------------------------------------------ + 2007 264.369524 88.185531 12.7231 +0.0 -29.7873 15.3006 -1.4973 + 2008 330.334676 89.119160 6.6450 +0.0 -20.4206 22.3215 -1.7980 + 3044 293.213801 43.333395 23.2208 -34.4 -31.7552 22.8198 14.9647 + 3045 257.384179 45.758052 23.7600 -34.4 -33.7186 12.8359 14.6527 + 3046 241.436091 50.884481 26.1075 -34.4 -34.8991 6.8759 14.5487 + 3047 242.756804 57.619234 18.6917 -34.4 -31.1404 9.3356 8.0915 + 3048 272.480606 51.260329 15.9696 -34.4 -29.5497 17.0862 8.0716 + 3049 293.161612 51.415627 16.0312 -34.4 -28.6287 21.4665 8.0763 + 3050 248.483832 78.157600 14.9904 -34.4 -30.7496 11.1797 1.1691 + +================================================================================ +BERECHNETE KOORDINATEN +================================================================================ +Punkt East [m] North [m] Elev [m] Methode +-------------------------------------------------------------------------------- +1001 22.7880 13.5648 -1.9522 ReflineSetup +1002 -20.8826 13.0076 -1.7418 Resection +1003 23.0704 14.0268 -1.9693 Resection +1004 -22.6960 19.7653 -1.9656 Resection +1005 -46.0315 23.5752 -1.9509 Resection +1006 22.9235 13.8783 -1.9035 Resection +1007 -21.6340 18.9681 -1.8149 Resection +1008 -46.5669 23.4502 -1.9004 Resection +1009 -17.1322 16.5482 -1.9002 Resection +2001 0.0135 11.7470 -1.3606 DirectReading +2002 20.2132 30.9638 -1.6635 DirectReading +2003 33.7824 13.0974 -2.6165 DirectReading +2004 20.4904 -15.4290 -1.4377 DirectReading +2005 -13.1379 18.3655 -1.7304 DirectReading +2006 -19.7621 10.5924 -1.6684 DirectReading +2007 -29.7873 15.3006 -1.4973 DirectReading +2008 -20.4206 22.3215 -1.7980 DirectReading +2009 -13.1436 12.6014 -1.8945 DirectReading +2010 -40.8710 25.2065 -1.8863 DirectReading +2011 -42.7361 16.0064 -1.4380 DirectReading +2012 -51.0074 10.0534 -1.9468 DirectReading +2013 -46.5963 26.4513 -1.9270 DirectReading +3001 33.8381 13.2197 -2.5746 DirectReading +3002 26.3084 30.1404 -1.9029 DirectReading +3003 26.3573 -3.2932 -2.1149 DirectReading +3004 0.0479 -0.5301 -3.0209 DirectReading +3005 -0.0383 6.7887 -3.4141 DirectReading +3006 1.1818 13.1309 1.8446 DirectReading +3007 0.8672 16.7446 1.8414 DirectReading +3008 -13.1745 11.0805 -0.2805 DirectReading +3009 -13.1684 10.7560 3.7827 DirectReading +3010 -14.0423 4.4805 1.8356 DirectReading +3011 -13.1661 13.7897 5.6887 DirectReading +3012 -13.1636 11.1912 7.5580 DirectReading +3013 -13.1644 10.6017 10.9218 DirectReading +3014 -10.4540 12.9684 -3.5927 DirectReading +3015 -5.7525 14.3185 -3.5971 DirectReading +3016 -2.1676 12.8421 -3.5976 DirectReading +3017 -16.9595 9.8712 -1.3065 DirectReading +3018 -13.1429 8.2567 -1.7125 DirectReading +3019 -21.1285 4.4806 -1.1057 DirectReading +3020 -31.1979 9.2528 -2.1284 DirectReading +3021 -29.6094 16.9228 -2.1847 DirectReading +3022 -30.1454 21.8961 -3.3677 DirectReading +3023 -20.8602 18.9426 -3.3759 DirectReading +3024 1.1879 12.8538 5.6065 DirectReading +3025 1.2132 3.9834 5.6149 DirectReading +3026 -0.0288 17.4009 9.3765 DirectReading +3027 0.0016 9.4491 13.1187 DirectReading +3028 -0.9148 16.7583 13.7277 DirectReading +3029 1.0963 3.3743 9.9548 DirectReading +3030 -41.6286 20.6712 -2.1988 DirectReading +3031 -42.3583 17.0694 -2.2065 DirectReading +3032 -43.9146 9.5389 -2.2176 DirectReading +3033 -39.0022 22.9320 -3.4510 DirectReading +3034 -2.2586 19.0099 16.7875 DirectReading +3035 -2.0392 11.6794 17.1551 DirectReading +3036 -2.4008 0.4984 16.9473 DirectReading +3037 -29.3024 18.1412 1.2646 DirectReading +3038 -30.2838 13.4734 -0.5015 DirectReading +3039 -30.2231 13.8426 2.9225 DirectReading +3040 -30.1643 14.1136 6.3263 DirectReading +3041 -30.2609 13.6479 9.5368 DirectReading +3042 -43.5965 11.3962 4.6592 DirectReading +3043 -41.0189 23.7766 4.6629 DirectReading +3044 -31.7552 22.8198 14.9647 DirectReading +3045 -33.7186 12.8359 14.6527 DirectReading +3046 -34.8991 6.8759 14.5487 DirectReading +3047 -31.1404 9.3356 8.0915 DirectReading +3048 -29.5497 17.0862 8.0716 DirectReading +3049 -28.6287 21.4665 8.0763 DirectReading +3050 -30.7496 11.1797 1.1691 DirectReading +5001 0.0000 -0.0000 0.0000 DirectReading +5002 0.0000 11.4075 -0.0352 DirectReading +6001 -18.1961 13.9129 -2.1769 DirectReading +6002 -15.6769 12.6476 -2.2853 DirectReading +6003 -26.3185 12.5495 -2.2740 DirectReading +6004 -24.7269 13.8469 -1.5604 DirectReading +6005 -25.6340 20.8780 -2.2433 DirectReading +6006 -48.1559 24.8057 -2.0923 DirectReading +6007 -48.5528 23.4885 -1.9831 DirectReading +6008 -42.8992 22.5856 -2.0952 DirectReading +7001 -13.1343 19.7463 -0.4237 DirectReading +7002 -7.4870 13.1927 -3.6751 DirectReading + +================================================================================ +Protokoll erstellt +================================================================================ \ No newline at end of file diff --git a/test_with_example.py b/test_with_example.py new file mode 100644 index 0000000..bc97a32 --- /dev/null +++ b/test_with_example.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python3 +""" +Test-Skript für das Trimble Geodesy Tool +Testet alle Module mit der Beispieldatei Baumschulenstr_93.jxl +""" + +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from modules.jxl_parser import JXLParser +from modules.cor_generator import CORGenerator +from modules.network_adjustment import NetworkAdjustment + +def test_jxl_parser(): + """Test des JXL-Parsers""" + print("=" * 80) + print("TEST 1: JXL-Parser") + print("=" * 80) + + parser = JXLParser() + success = parser.parse("test_data/Baumschulenstr_93.jxl") + + if not success: + print("❌ FEHLER: Datei konnte nicht geladen werden!") + return None + + print(f"✓ Datei erfolgreich geladen: {parser.job_name}") + print(f"✓ Koordinatensystem: {parser.coordinate_system}") + print(f"✓ Winkeleinheit: {parser.angle_units}") + print() + + # Stationen + print(f"Stationen: {len(parser.stations)}") + for sid, station in sorted(parser.stations.items(), key=lambda x: x[1].timestamp): + print(f" - {station.name}: {station.station_type}") + if station.east is not None: + print(f" Koordinaten: E={station.east:.4f}, N={station.north:.4f}, H={station.elevation or 0:.4f}") + print() + + # Targets/Prismenkonstanten + print(f"Prismenkonstanten (eindeutige):") + seen = set() + for tid, target in parser.targets.items(): + key = (target.prism_type, target.prism_constant) + if key not in seen: + seen.add(key) + print(f" - {target.prism_type}: {target.prism_constant*1000:+.1f} mm") + print() + + # Punkte + active = parser.get_active_points() + print(f"Aktive Punkte: {len(active)}") + print() + + # Referenzpunkte (Festpunkte) + ref_points = parser.get_reference_points() + print(f"Referenzpunkte (Passpunkte): {ref_points}") + + # Standpunkte + station_points = parser.get_station_points() + print(f"Standpunkte: {station_points}") + + # Messpunkte + meas_points = parser.get_measurement_points() + print(f"Messpunkte (erste 10): {meas_points[:10]}...") + print() + + # Messungen für erste Station + first_station_id = list(parser.stations.keys())[0] + first_station = parser.stations[first_station_id] + measurements = parser.get_detailed_measurements_from_station(first_station_id) + + print(f"Messungen von Station {first_station.name}:") + backsight = [m for m in measurements if m.classification == 'BackSight' and not m.deleted] + normal = [m for m in measurements if m.classification != 'BackSight' and not m.deleted] + + print(f" Anschlussmessungen: {len(backsight)}") + for m in backsight: + print(f" - {m.point_name}: Hz={m.horizontal_circle:.6f}, V={m.vertical_circle:.6f}, D={m.edm_distance:.4f}") + + print(f" Normale Messungen: {len(normal)}") + for m in normal[:5]: # Nur erste 5 + print(f" - {m.point_name}: Hz={m.horizontal_circle:.6f}, V={m.vertical_circle:.6f}, D={m.edm_distance:.4f}") + + print() + return parser + + +def test_cor_generator(parser): + """Test des COR-Generators""" + print("=" * 80) + print("TEST 2: COR-Generator") + print("=" * 80) + + generator = CORGenerator(parser) + points = generator.generate_from_computed_grid() + + print(f"✓ {len(points)} Punkte generiert") + print() + + # Vergleich mit Referenz-COR + print("Vergleich mit Baumschulenstr_93.cor:") + + # Lade Referenz + ref_points = {} + with open("test_data/Baumschulenstr_93.cor", 'r') as f: + for line in f: + line = line.strip() + if not line or line.startswith('|--'): + continue + parts = line.replace('|', ',').split(',') + parts = [p.strip() for p in parts if p.strip()] + if len(parts) >= 4: + name = parts[0] + try: + x = float(parts[1]) + y = float(parts[2]) + z = float(parts[3]) + ref_points[name] = (x, y, z) + except: + continue + + # Vergleiche + matches = 0 + mismatches = [] + for p in points: + if p.name in ref_points: + ref_x, ref_y, ref_z = ref_points[p.name] + dx = abs(p.x - ref_x) + dy = abs(p.y - ref_y) + dz = abs(p.z - ref_z) + + if dx < 0.01 and dy < 0.01 and dz < 0.01: + matches += 1 + else: + mismatches.append((p.name, dx, dy, dz)) + + print(f" Übereinstimmungen: {matches}") + print(f" Abweichungen: {len(mismatches)}") + if mismatches[:5]: + print(f" Erste Abweichungen:") + for name, dx, dy, dz in mismatches[:5]: + print(f" - {name}: ΔX={dx:.3f}, ΔY={dy:.3f}, ΔZ={dz:.3f}") + print() + + return generator + + +def test_calculation_protocol(parser): + """Test des Berechnungsprotokolls""" + print("=" * 80) + print("TEST 3: Berechnungsprotokoll") + print("=" * 80) + + protocol = parser.get_calculation_protocol() + lines = protocol.split('\n') + + print(f"✓ Protokoll generiert ({len(lines)} Zeilen)") + print() + + # Zeige Auszug + print("Auszug:") + for line in lines[:30]: + print(f" {line}") + print(" ...") + print() + + # Speichere Protokoll + with open("test_data/berechnungsprotokoll_test.txt", 'w') as f: + f.write(protocol) + print("✓ Protokoll gespeichert: test_data/berechnungsprotokoll_test.txt") + print() + + +def test_network_adjustment(parser): + """Test der Netzausgleichung""" + print("=" * 80) + print("TEST 4: Netzausgleichung (KORREKTES KONZEPT)") + print("=" * 80) + + adjustment = NetworkAdjustment(parser) + + # Beobachtungen extrahieren + obs = adjustment.extract_observations() + print(f"✓ {len(obs)} Beobachtungen extrahiert") + + # Punkte initialisieren + adjustment.initialize_points() + print(f"✓ {len(adjustment.points)} Punkte initialisiert") + + # KORREKTES KONZEPT: Nur Passpunkte als Festpunkte + ref_points = parser.get_reference_points() + print(f"\nFestpunkte (Passpunkte): {ref_points}") + + for name in ref_points: + if name in adjustment.points: + adjustment.set_fixed_point(name) + + print(f"✓ {len(adjustment.fixed_points)} Festpunkte gesetzt") + + # Neupunkte (Standpunkte) - eindeutige Namen + station_points = list(set(s.name for s in parser.stations.values())) + new_points = [p for p in station_points if p not in adjustment.fixed_points] + print(f"Neupunkte (Standpunkte): {new_points}") + + # Messpunkte + meas_points = [p for p in adjustment.points.keys() + if p not in adjustment.fixed_points and p not in station_points] + print(f"Messpunkte: {len(meas_points)}") + + try: + result = adjustment.adjust() + print() + print(f"✓ Ausgleichung abgeschlossen:") + print(f" Iterationen: {result.iterations}") + print(f" Konvergiert: {result.converged}") + print(f" Sigma-0: {result.sigma_0_posteriori:.4f}") + print(f" Redundanz: {result.redundancy}") + print() + + # Ausgeglichene Standpunkte + print("Ausgeglichene Standpunkte:") + for name in new_points: + if name in adjustment.points: + p = adjustment.points[name] + print(f" {name}: X={p.x:.4f}, Y={p.y:.4f}, Z={p.z:.4f}, σPos={p.std_position*1000:.1f}mm") + + return adjustment + + except Exception as e: + print(f"❌ FEHLER bei Ausgleichung: {e}") + import traceback + traceback.print_exc() + return None + + +def main(): + print("\n" + "=" * 80) + print("TRIMBLE GEODESY TOOL - UMFANGREICHE TESTS") + print("Testdatei: Baumschulenstr_93.jxl") + print("=" * 80 + "\n") + + # Test 1: Parser + parser = test_jxl_parser() + if not parser: + return + + # Test 2: COR-Generator + test_cor_generator(parser) + + # Test 3: Berechnungsprotokoll + test_calculation_protocol(parser) + + # Test 4: Netzausgleichung + test_network_adjustment(parser) + + print("\n" + "=" * 80) + print("TESTS ABGESCHLOSSEN") + print("=" * 80 + "\n") + + +if __name__ == "__main__": + main()