diff --git a/__pycache__/mag_decl_webscrape.cpython-311.pyc b/__pycache__/mag_decl_webscrape.cpython-311.pyc index be0771d..6069a6d 100644 Binary files a/__pycache__/mag_decl_webscrape.cpython-311.pyc and b/__pycache__/mag_decl_webscrape.cpython-311.pyc differ diff --git a/__pycache__/speleoliti_handler.cpython-311.pyc b/__pycache__/speleoliti_handler.cpython-311.pyc new file mode 100644 index 0000000..a121e32 Binary files /dev/null and b/__pycache__/speleoliti_handler.cpython-311.pyc differ diff --git a/config_settings.json b/config_settings.json index bd136c8..e06e561 100644 --- a/config_settings.json +++ b/config_settings.json @@ -18,8 +18,8 @@ "Calculate declination" ], "parse_file": [ - "Generiraj CSV", - "Generate CSV" + "Pohrani u CSV", + "Save to CSV" ], "readme_file_path": [ "surveyscraper_README.txt", @@ -29,17 +29,13 @@ "Pogre\u0161ka prilikom u\u010ditanja readme datoteke!", "Error while reading readme file!" ], - "file_path_csv": [ - "Otvori csv datoteku", - "Open csv file" - ], - "file_path_txt": [ - "Otvori txt datoteku", - "Open txt file" + "file_path": [ + "Otvori datoteku", + "Open file" ], "file_path_error": [ "Nije otvorena pravilna datoteka!", - "Incorrect file was openned!" + "Incorrect file was opened!" ], "success_run_true": [ "Uspje\u0161na konverzija!", @@ -53,7 +49,11 @@ "Bez predznaka Excel \u0107e prepoznati to\u010dke kao brojeve i maknuti nule!", "Without prefix Excel will read shots as numbers!" ], - "write_file_path": [ + "empty_write_path": [ + "Putanja do datoteke za pohranu je prazna!", + "The file write path is empty!" + ], + "write_csv_file_path": [ "Pohrani csv datoteku", "Save csv" ], @@ -69,10 +69,6 @@ "Nije mogu\u0107e kreirati csv datoteku! Molim zatvori datoteku!", "Unable to write to a csv file! Please close the file!" ], - "gui_md.title": [ - "Magnetska deklinacija", - "Magnetic declination" - ], "location_error": [ "Prazan unos!", "Empty input!" diff --git a/mag_decl_webscrape.py b/mag_decl_webscrape.py index 1f0859f..febe1c4 100644 --- a/mag_decl_webscrape.py +++ b/mag_decl_webscrape.py @@ -1,17 +1,27 @@ +import ast +import json import requests class Retrieve_lat_lon(): def __init__(self, location): self.location = location + self.user_agent = "SurveyScraper/3.0 (https://github.com/LovelK7/SurveyScraper)" def retrieve_lat_lon(self): """retreive location coordinates through API with requests""" - url_location = (f"https://nominatim.openstreetmap.org/search?q={self.location}&format=json") - response = requests.get(url_location) - json_result = response.json() - latitude = float(json_result[0]['lat']) - longitude = float(json_result[0]['lon']) - return latitude, longitude + + url_location = f"https://nominatim.openstreetmap.org/search.php?q={self.location}&format=jsonv2" + headers = {'User-Agent': self.user_agent} + response = requests.get(url_location, headers=headers) + + if response.status_code == 200: + json_result = response.json() + latitude = float(json_result[0]['lat']) + longitude = float(json_result[0]['lon']) + return latitude, longitude + else: + print(f'Erorr while accessing nominatim.openstreetmap! \n {response.text}') + return None, None class Retrieve_magn_decl(): def __init__(self, latitude, longitude, model, year, month, day): @@ -24,17 +34,21 @@ def __init__(self, latitude, longitude, model, year, month, day): def retrieve_magn_decl(self): """retreive magnetic declination through API with requests""" - url_decl = (f"https://www.ngdc.noaa.gov/geomag-web/calculators/calculateDeclination?lat1={self.latitude}&lon1={self.longitude}" - f"&model={self.model}&startYear={self.year}&startMonth={self.month}&startDay={self.day}&key=zNEw7&resultFormat=json") - response = requests.get(url_decl) - json_result = response.json() - magnetic_declination = json_result['result'][0]['declination'] - return magnetic_declination + if self.latitude and self.longitude: + url_decl = (f"https://www.ngdc.noaa.gov/geomag-web/calculators/calculateDeclination?lat1={self.latitude}&lon1={self.longitude}" + f"&model={self.model}&startYear={self.year}&startMonth={self.month}&startDay={self.day}&key=zNEw7&resultFormat=json") + response = requests.get(url_decl) + json_result = response.json() + magnetic_declination = json_result['result'][0]['declination'] + return magnetic_declination + else: + return None #*************** ONLY FOR TESTING ************************ -# if __name__ == '__main__': -# location, model, year, month, day = 'krkuz','IGRF','2022','6','16' -# lat_lon_app = Retrieve_lat_lon(location) -# latitude, longitude = lat_lon_app.retrieve_lat_lon() -# magn_decl_app = Retrieve_magn_decl(latitude, longitude, model, year, month, day) -# print(magn_decl_app.retrieve_magn_decl()) \ No newline at end of file +if __name__ == '__main__': + + location, model, year, month, day = 'krkuz','IGRF','2022','6','16' + lat_lon_app = Retrieve_lat_lon(location) + latitude, longitude = lat_lon_app.retrieve_lat_lon() + magn_decl_app = Retrieve_magn_decl(latitude, longitude, model, year, month, day) + print(magn_decl_app.retrieve_magn_decl()) \ No newline at end of file diff --git a/speleoliti_handler.py b/speleoliti_handler.py new file mode 100644 index 0000000..54087d4 --- /dev/null +++ b/speleoliti_handler.py @@ -0,0 +1,142 @@ +import os +import sys +import time +from tkinter import messagebox +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.support.wait import WebDriverWait +from selenium.webdriver.support import expected_conditions +from selenium.webdriver.chrome.service import Service +from webdriver_manager.chrome import ChromeDriverManager + +class Speleoliti_online(): + """ + This script handles Speleoliti Online (https://speleoliti.speleo.net/online/app_en.html) + Methods: constructor, open_object, retrieve_cave_data, find_highest_point, update_fixed_station + """ + def __init__(self, headless, survey_path=None): + """Initialize Selenium ChromeDriver""" + + self.online = True + try: + driver_path = ChromeDriverManager().install() + except: # Exception as e: + #messagebox.showerror('Error', f'Error accessing Speleoliti Online !\n\n{e} \n\n Try using SurveyScraper offline!') + self.online = False + #driver_path = 'c:/Users/Lovel.IZRK-LK-NB/.wdm/drivers/chromedriver/win64/120.0.6099.225/chromedriver-win32/chromedriver.exe' + if self.online: + service = Service(driver_path) + options = webdriver.ChromeOptions() + self.headless = headless + if headless: + options.add_argument("--headless=new") + try: + self.driver = webdriver.Chrome(service=service, options=options) + except Exception as e: + messagebox.showerror('Error', f'Error initializing ChromeDriver: !\n\n{e}') + self.driver.quit() + if not headless: + self.driver.minimize_window() + self.handle_of_the_window = self.driver.current_window_handle + self.url = f"https://speleoliti.speleo.net/online/app_en.html" + # Access survey data file + survey_data_filename = 'survey_data.json' + if getattr(sys,'frozen', False): #check if the app runs as a script or as a frozen exe file + self.survey_data_filepath = os.path.join(os.path.dirname(sys.executable), survey_data_filename) + elif __file__: + self.survey_data_filepath = os.path.join(os.path.dirname(__file__), survey_data_filename) + else: + self.survey_data_filepath = survey_path + + def open_empty_object(self): + """Opens an empty window of Speleoliti Online""" + self.driver.get(self.url) + + def open_object(self): + """Opens a cave survey file into Speleoliti Online""" + try: + self.driver.get(self.url) + WebDriverWait(self.driver, 3).until(expected_conditions.element_to_be_clickable((By.XPATH, '//*[@id="DefOpt2"]'))).click() + self.driver.find_element("xpath",'//*[@id="impexRadio_file"]').click() # find file import radiobutton + file_input = self.driver.find_element("xpath",'//*[@id="UploadFileFld"]') # insert cave survey file path + file_input.send_keys(self.survey_data_filepath) # direct input of json file content to an input field + self.driver.find_element("xpath",'//*[@id="impex"]/table/tbody/tr/td/div/input').click() # confirm import + WebDriverWait(self.driver, 3).until(expected_conditions.alert_is_present()) + self.driver.switch_to.alert.accept() # accept alert message + except Exception as e: + messagebox.showerror('Error', f'Error while opening object!\n\n{e}') + return e + + def retrieve_cave_data(self): + """retrieve survey data from""" + poly_length = self.driver.find_element("xpath",'//*[@id="table99"]/tbody/tr[4]/td[2]').text.split()[0] + hor_length = self.driver.find_element("xpath",'//*[@id="table99"]/tbody/tr[5]/td[2]').text.split()[0] + elevation = self.driver.find_element("xpath",'//*[@id="table99"]/tbody/tr[6]/td[2]').text.split()[0] + depth = self.driver.find_element("xpath",'//*[@id="table99"]/tbody/tr[7]/td[2]').text.split()[0] + return poly_length, hor_length, elevation, depth + + def find_highest_point(self): + """Find highest elevated station so it becomes the fixed station by default""" + try: + WebDriverWait(self.driver, 3).until(expected_conditions.element_to_be_clickable((By.XPATH, '//*[@id="ico_coords"]'))).click() + #self.driver.find_element("xpath",'//*[@id="ico_coords"]').click() # switch to coordinate table + data_tbl = self.driver.find_element("xpath",'//*[@id="table2b"]/tbody') + station_alts = {} + for index, row in enumerate(data_tbl.find_elements('xpath','.//tr')): + if index == 0: + continue + station = row.find_element('xpath',f'//*[@id="table2b"]/tbody/tr[{index+1}]/td[1]').text + alt = row.find_element('xpath',f'//*[@id="table2b"]/tbody/tr[{index+1}]/td[4]/div').text + station_alts[station] = float(alt) + self.driver.find_element("xpath",'//*[@id="ico_main"]').click() # return to main menu + key_of_max_value = max(station_alts, key=station_alts.get) + return key_of_max_value + except Exception as e: + messagebox.showerror('Error', f'Error finding the highest point!\n\n{e}') + return e + + def update_fixed_station(self, fixed_station): + """Update the given fixed station""" + try: + self.driver.find_element("xpath",'//*[@id="ico_survey"]').click() # switch to edit survey table + fixed_station_elm = self.driver.find_element("xpath",'//*[@id="survey_fix"]') # find fixed station element + self.driver.execute_script('arguments[0].value = ""', fixed_station_elm) # delete current entry + fixed_station_elm.click() + fixed_station_elm.send_keys(fixed_station) # insert new entry + self.driver.find_element("xpath",'//*[@id="ico_main"]').click() # return to main menu + except Exception as e: + messagebox.showerror('Error', f'Error updating the fixed station!\n\n{e}') + return e + + def restore_window(self): + self.driver.switch_to.window(self.handle_of_the_window) + self.driver.set_window_rect(0, 0) + + def close_driver(self): + if self.driver: + self.driver.quit() + +#*************** ONLY FOR TESTING ************************ +if __name__ == '__main__': + + curr_time = time.time() + # Set path to a current cave survey data + survey_data_filename = 'survey_data.json' + if getattr(sys,'frozen', False): #check if the app runs as a script or as a frozen exe file + survey_data_filepath = os.path.join(os.path.dirname(sys.executable), survey_data_filename) + elif __file__: + survey_data_filepath = os.path.join(os.path.dirname(__file__), survey_data_filename) + + speleoliti_app = Speleoliti_online(headless=False, survey_path=survey_data_filepath) + if speleoliti_app.online: + speleoliti_app.open_object() + fixed_station = speleoliti_app.find_highest_point() + speleoliti_app.update_fixed_station(fixed_station) + dimensions = speleoliti_app.retrieve_cave_data() + #speleoliti_app.restore_window() + speleoliti_app.close_driver() + print(dimensions) + duration = time.time() - curr_time + print('Duration: ',duration) + else: + print('Unable to reach Speleoliti Online due to internet connection!') \ No newline at end of file diff --git a/survey_data.json b/survey_data.json new file mode 100644 index 0000000..c038918 --- /dev/null +++ b/survey_data.json @@ -0,0 +1,51 @@ +{ + "fix": "ed3", + "x": "", + "y": "", + "z": "", + "dcl": 4.332, + "name": "jama_Edison_2", + "descr": "ed", + "viz": [ + "null", + { + "t1": "ed1", + "t2": "ed0", + "l": 6.13, + "a": 61.93, + "f": -82.4, + "left": "null", + "right": "null", + "up": "null", + "down": "null", + "note": "", + "flags": "" + }, + { + "t1": "ed1", + "t2": "ed2", + "l": 4.02, + "a": 122.83, + "f": 71.9, + "left": "null", + "right": "null", + "up": "null", + "down": "null", + "note": "", + "flags": "" + }, + { + "t1": "ed2", + "t2": "ed3", + "l": 1.91, + "a": 287.83, + "f": 66.6, + "left": "null", + "right": "null", + "up": "null", + "down": "null", + "note": "", + "flags": "" + } + ] +} \ No newline at end of file diff --git a/surveyscraper_README.txt b/surveyscraper_README.txt index 414fbdd..0811dad 100644 --- a/surveyscraper_README.txt +++ b/surveyscraper_README.txt @@ -1,58 +1,69 @@ +************************************************************************************ -SurveyScraper + SurveyScraper - Verzija 3.0 (veljača 2024.) -***************************************************************************************************** +************************************************************************************ +************************ ŠTO JE SURVEYSCRAPER? **************************** -Verzija 2.2 (rujan 2023) -Autor: Lovel Kukuljan +SurveyScraper je program koji pojednostavljuje obradu podataka mjernih vlakova dobivenih iz najčešće korištenih programa za digitalno topografsko crtanje: TopoDroid, Qave i PocketTopo. Program u suštini zamjenjuje funkcionalnosti MS Excela za obradu mjernih vlakova te koristi funkcionalnosti Speleolita za izračun osnovnih dimenzija nacrta ili speleološkog objekta. -************************************** ŠTO JE SURVEYSCRAPER? ************************************** +************************* FUNKCIONALNOSTI ******************************** -SurveyScraper je jednostavan program koji pojednostavljuje obradu podataka mjernih vlakova. -Program učitava kreirane tablice mjernih vlakova iz TopoDroid-a ili PocketTopo-a, filtrira samo -glavne vlakove te ih pohranjuje u novu csv datoteku. Tada je moguće lako kopirati te podatke u -Speleolite za izračun dimenzija speleološkog objekta. Dodatna funkcionalnost je uračunavanje -magnetske deklinacije odnosno korekcije azimuta vlakova, a prema odabranoj lokaciji, modelu i datumu. +-- Uvoz mjernih vlakova i automatsko filtriranje glavnih vlakova. +-- Dodavanje predznaka/prefiksa točkama koje se pohranjuju u csv datoteci. +-- Korekcija azimuta vlakova prema unesenoj ili izračunatoj magnetskoj deklinaciji. +-- Računanje magnetske deklinacije za odabranu lokaciju, model i datum. +-- Pohrana podataka i tablice mjernih vlakova u csv datoteku. +-- Mogućnost dodatnih funkcionalnosti otvaranjem Speleoliti Online -************************************* UPUTE ZA KORIŠTENJE ************************************* +************************* OPIS SUČELJA ********************************* -1. Odaberi program koji si koristio za crtanje i iz kojeg si kreirao datoteku vlakova (TopoDroid ili PocketTopo) +Program se sastoji od POČETNE TRAKE: +-- Speleoliti Online: otvaranje preglednika Google Chrome-a i stranice Speleoliti Online (https://www.speleo.net/speleoliti/online/app.html), +-- HR: odabir jezika (trenutno je dostupan jedino hrvatski jezik), +te TRI TABA: +-- Main: obrada mjernih vlakova, +-- MagDec: izračun magnetske deklinacije, +-- Help: otvaranje ovih uputa unutar programa. -2. Otvori datoteku s vlakovima - .csv datoteka za TopoDroid / .txt datoteka za PocketTopo +************************* UPUTE ZA KORIŠTENJE **************************** -3. Definiraj predznak točaka (opcijski) - Npr. prema autoru nacrta (Marko Markić - "mm") ili imenu objekta (Markotova špilja - "mš") - Tada će za prvi slučaj točke dobiti sljedeći predznak: +1. UVOZ +-- U Main tab-u otvori datoteku s vlakovima: csv datoteku TopoDroid-a, srv datoteku Qave-a ili txt datoteku PocketTopo-a. + +2. Definiraj prefiks/predznak točaka (opcionalno) +-- Npr. prema autoru nacrta (Marko Markić - "mm-") ili imenu objekta (Markićeva špilja - "mš-"). Tada će za slučajeve TopoDroid-a i Qave-a točke imati nazive: mm-0 mm-1 mm-1 mm-2 ... - ili +-- Odnosno za PocketTopo: mm-1.0 mm-1.1 mm-1.1 mm-1.2 ... -4. Uračunaj magnetsku deklinaciju (opcijski). Otvara se zaseban prozor za računanje. - 4.1 Upiši lokaciju speleološkog objekta (npr. najbliže mjesto ili grad, mjesto, ulica) - 4.2 Dohvati koordinate. Tražilica će locirati upisano mjesto te upisati koordinate u sljedeća dva polja - (geografska širina i geografska dužina). Pronađena lokacija biti će prikazana na umanjenom zemljovidu. - 4.3 Koordinate je moguće upisati i samostalno (WGS84, decimalni zapis s točkom npr. 45.123 i 14.123) - 4.4 Odaberi željeni model za računanje magnetske deklinacije (default je WMM) - 4.5 Odaberi datum odnosno dan, mjesec i godinu mjerenja - 4.6 Izračunaj. Ukoliko je sve pravilno upisano, generirati će se magnetska deklinacija na tri decimalna mjesta. - 4.7 Izračunaj. Prozor će se zatvoriti, a izračunana magnetske deklinacije će biti zapamćena te prikazana - na glavnom prozoru. +3. Unesi magnetsku deklinaciju u decimalnim stupnjevima ili ju izračunaj automatski (opcionalno) +-- 3.1 Za izračun magnetske deklinacije pritisni tab MagDec +-- 3.2 Upiši lokaciju speleološkog objekta (npr. najbliže mjesto ili grad, mjesto, ulica) +-- 3.3 Dohvati koordinate. Tražilica će locirati upisano mjesto te upisati koordinate u sljedeća dva polja (geografska širina i geografska dužina). Pronađena lokacija biti će prikazana na umanjenom zemljovidu. +-- 3.4 Koordinate je moguće upisati i samostalno (WGS84, decimalni zapis s točkom npr. 45.12345 i 14.12345) +-- 3.5 Odaberi željeni model za računanje magnetske deklinacije (default: WMM) +-- 3.6 Odaberi datum odnosno dan, mjesec i godinu mjerenja (default: današnji datum) +-- 3.7 Izračunaj. Ukoliko je sve pravilno upisano, dohvatiti će se magnetska deklinacija na tri decimalna mjesta sa portala Magnetic Field Calculators (NOAA, https://www.ngdc.noaa.gov/geomag/calculators/magcalc.shtml). Nakon izračuna, magnetska deklinacija će biti automatski upisana u polje za magnetsku korekciju u Main tab-u. + +4. Upiši dodatne podatke o speleološkom objektu (opcionalno) +-- Desni dio Main prozora omogućuje unos imena objekta, koordinate i nadmorske visine ulaza, definiranje imena izlazne datoteke te definiranje fiksne točke. +-- Prilikom obrade vlakova visinski najviša točka postaje određena kao fiksna, čime mjera "Dubina od fiksne točke" ujedno postaje i dubina objekta. +(Trenutno, promjena fiksne točke neće automatski promjeniti i dubinu) -5. Generiraj CSV - Odaberi mjesto za pohranu CSV datoteke koja će automatski dobiti sufiks "glavni_vlakovi". - Ukoliko je definirana magnetska deklinacija, tada će biti uračunata, ukoliko nije, biti će 0. - 5.1 Ispisati će se poruka da li je postupak uspješan ili ne - 5.2 Pojaviti će se mogućnost otvaranja generirane CSV datoteke +5. Pohrani postavke +-- Ukoliko je unesen predznak točaka, magnetska deklinacija i/ili ostali podaci o objektu, potrebno je pohraniti iste pritiskom na "Pohrani postavke". -**************************************** PROMJENA JEZIKA **************************************** +6. IZVOZ +-- Pritiskom na "Izvezi u CSV" otvara se prozor za odabir mjesta za pohranu csv datoteke. Izlazna datoteka se sastoji od redova s podacima o objektu te tablice glavnih mjernih vlakova. +-- Opcija "Dodaj stupac izvornih azimuta" - Ukoliko je definirana magnetska deklinacija, moguće je aktivirati opciju izvoza stupca i originalnih azimuta. -Jezik programa se mijenja postavkom na vrhu glavnog prozora. Primjena novog jezika moguća je jedino ponovnim -otvaranjem programa. +******************************* KONTAKT *********************************** -***************************************************************************************************************** +Za sve dojave bug-ova, komentare i prijedloge poboljšanja, slobodno me kontaktiraj na lkukuljan7 (at) gmail.com. -Za sve komentare ili prijedloge poboljšanja, slobodno me kontaktiraj na lkukuljan7 (at) gmail.com - +************************************************************************************ diff --git a/surveyscraper_README_en.txt b/surveyscraper_README_en.txt deleted file mode 100644 index 97897d8..0000000 --- a/surveyscraper_README_en.txt +++ /dev/null @@ -1,57 +0,0 @@ -SurveyScraper - -************************************************************************************************************* - -Version 2.2 (September 2023) -Author: Lovel Kukuljan - -************************************* WHAT IS SURVEYSCRAPER? ****************************************** - -SurveyScraper is a simple program that simplifies the processing of cave survey data. -The program loads created tables of measuring shots from TopoDroid or PocketTopo, filters only -main shots and stores them in a new csv file. Then it is possible to easily copy this data to other software -for calculating the dimensions of a cave. Additional functionality is the calculation of magnetic declination -and dicrection corrections of shots, according to the selected location, model and date. - -************************************* INSTRUCTIONS FOR USE ************************************ - -1. Choose the program you used for drawing and from which you created the survey file (TopoDroid or PocketTopo) - -2. Open the shots file - .csv file for TopoDroid / .txt file for PocketTopo - -3. Define the sign of the points (optional) - For example, according to the author of the survey (John Doe - "JD") or the name of the cave (Doe's cave - "dc") - Then, for the first case, the points will get the following sign: - JD-0 JD-1 - JD-1 JD-2 - ... - or - JD-1.0 JD-1.1 - JD-1.1 JD-1.2 - ... - -4. Include magnetic declination (optional). A separate calculation window opens. - 4.1 Enter the location of the cave (e.g. the nearest town or city, place, street) - 4.2 Get coordinates. The search engine will locate the entered place and enter the coordinates in the following - two fields (latitude and longitude). The found location will be displayed on a reduced map. - 4.3 Coordinates can be entered independently (WGS84, decimal notation, e.g. 45.123 and 14.123) - 4.4 Select the desired model (default is WMM) - 4.5 Select the date or day, month and year of the survey - 4.6 Calculate. If everything is entered correctly, the magnetic declination will be generated to three decimal places. - 4.7 Apply. The window will close, and the calculated magnetic declinations will be saved and shown on the main screen. - -5. Generate CSV - Select a location to store the CSV file, which will automatically receive the suffix "main_shots". - If the magnetic declination is defined, then it will be included, if not, it will be 0. - 5.1 A message will be printed whether the procedure is successful or not - 5.2 The option to open the generated CSV file will appear - -****************************************** CHANGE LANGUAGE ****************************************** - -The language of the program is changed by the dropdown setting at the top of the main window. The new language is applied -after reopening the program. - -************************************************************************************************************ - -For any comments or suggestions for improvement, feel free to contact me at lkukuljan7 (at) gmail.com - -************************************************************************************************************ \ No newline at end of file diff --git a/surveyscraper_v2.py b/surveyscraper_v2.py deleted file mode 100644 index aac25c7..0000000 --- a/surveyscraper_v2.py +++ /dev/null @@ -1,442 +0,0 @@ -import re -import os -import sys -import json -import tkintermapview -import datetime -import winsound -import subprocess -import customtkinter as ctk -from PIL import Image -from tkinter import messagebox -from idlelib.tooltip import Hovertip -from mag_decl_webscrape import Retrieve_lat_lon, Retrieve_magn_decl - -ctk.set_appearance_mode('light') -ctk.set_default_color_theme('green') - -def config(): - """Read configuration settings""" - global application_path - """Set the directory of the original file for a path to any other file""" - if getattr(sys,'frozen',False): #check if the app runs as a script or as a frozen exe file - application_path = os.path.dirname(sys.executable) - elif __file__: - application_path = os.path.dirname(__file__) - - """Read language catalog""" - global lcat - file_path = os.path.join(application_path, 'config_settings.json') - try: - with open(file_path,'r', encoding='utf-8') as file: - lcat = json.load(file) - return lcat['language_setting'] - except Exception as ex: - messagebox.showerror('Error',f'There was an error while reading the file: {ex}') - -class SurveyScraper(): - def __init__(self, lc): - self.gui = ctk.CTk() - self.gui.title('SurveyScraper') - self.dim_h = 330 - self.gui.geometry(f"250x{self.dim_h}+200+200") - self.gui.columnconfigure(0, weight=6) - self.gui.columnconfigure(1, weight=1) - self.md_val = 0 - if lc == 'HR': - self.lc = 0 - elif lc == 'EN': - self.lc = 1 - - def resource_path(relative_path): - """ Get absolute path to resource, works for dev and for PyInstaller """ - try: - # PyInstaller creates a temp folder and stores path in _MEIPASS - base_path = sys._MEIPASS - except Exception: - #base_path = os.path.abspath(".") - base_path = os.path.dirname(__file__) - return os.path.join(base_path, relative_path) - - # MAIN FRAME - self.lang_var = ctk.StringVar(self.gui) - self.lang_var.set(lc) - self.gui_title = ctk.CTkLabel(self.gui, text='SurveyScraper', font=('Roboto', 18)) - self.gui_title.grid(row=0, column=0, rowspan=2, padx=0, pady=5, sticky='ew') - language_choice = ctk.CTkOptionMenu(self.gui, values=['HR','EN'], variable=self.lang_var, anchor='center', font=('Roboto', 12), width=20, - command=self.set_language) - language_choice.grid(row=0, column=1, padx=0, pady=5, sticky='w') - self.readme = ctk.CTkButton(self.gui, text='?', width=20, font=('Roboto', 12), command=self.open_readme) - self.readme.grid(row=0, column=2, padx=5, pady=5, sticky='e') - - # SOFTWARE FRAME - self.software_frm = ctk.CTkFrame(self.gui) - self.software_frm.grid(row=2, column=0, columnspan=3, padx=10, pady=10, sticky='ew') - self.software_lbl = ctk.CTkLabel(self.software_frm, text=f'{lcat["software_lbl"][self.lc]}', font=('Roboto', 14)) - self.software_lbl.pack(padx=5, pady=5) - self.software = ctk.StringVar(self.software_frm) - self.software.set(lcat['last_used_software']) - software_choice = ctk.CTkOptionMenu(self.software_frm, values=['TopoDroid','PocketTopo'], variable=self.software, anchor='center') - software_choice.pack(padx=5, pady=5) - - # OPEN FILE-DECLINATION FRAME - self.content = ctk.CTkFrame(self.gui) - self.content.grid(row=3, column=0, columnspan=3, padx=10, pady=5, sticky='ew') - self.content.grid_columnconfigure(0, weight=1) - self.content.grid_columnconfigure(1, weight=1) - self.open_file_img = ctk.CTkImage(Image.open(resource_path('img/open_file.png')), size=(15,15)) - self.open_file = ctk.CTkButton(self.content, width=180, text=f'{lcat["open_file"][self.lc]}', - command=lambda: self.open_file_event(self.software.get()), compound='left', image=self.open_file_img) - self.open_file.grid(row=0, column=0, padx=5, pady=10, columnspan=2) - self.opened_file = ctk.CTkLabel(self.content, text='', width=200, height=25, fg_color='white', corner_radius=5) - self.opened_file.grid(row=1, column=0, padx=5, pady=5, columnspan=2) - self.opened_file.configure(state='disabled') - self.opened_file.grid_remove() - self.survey_sign_lbl = ctk.CTkLabel(self.content, text=f'{lcat["survey_sign_lbl"][self.lc]}') - self.survey_sign_lbl.grid(row=2, column=0, padx=5, pady=5, sticky='e') - self.survey_sign = ctk.CTkEntry(self.content, width=50, height=25) - self.survey_sign.grid(row=2, column=1, padx=5, pady=5, sticky='w') - self.calc_md_img = ctk.CTkImage(Image.open(resource_path('img/compass.png')), size=(15,15)) - self.calc_magn_decl_btn = ctk.CTkButton(self.content, width=200, text=f'{lcat["calc_magn_decl_btn"][self.lc]}', - command=self.calc_magn_decl, compound='left', image=self.calc_md_img) - self.calc_magn_decl_btn.grid(row=3, column=0, padx=5, pady=10, columnspan=2) - self.show_md_value_lbl = ctk.CTkLabel(self.content, text=f'{lcat["show_md_value_lbl"][self.lc]}:') - self.show_md_value_lbl.grid(row=4, column=0, padx=5, pady=5, sticky='e') - self.show_md_value_lbl.grid_remove() - self.show_md_value = ctk.CTkEntry(self.content, width=50, height=25, fg_color='white', corner_radius=5) - self.show_md_value.grid(row=4, column=1, padx=5, pady=5, sticky='w') - self.show_md_value.grid_remove() - - # GENERATE FRAME - self.content2 = ctk.CTkFrame(self.gui) - self.content2.grid(row=4, column=0, columnspan=3, padx=10, pady=5, sticky='ew') - self.parse_img = ctk.CTkImage(Image.open(resource_path('img/generate.png')), size=(15,15)) - self.parse_file = ctk.CTkButton(self.content2, text=f'{lcat["parse_file"][self.lc]}', - command=lambda: self.parse_event(self.software.get()), - compound='left', image=self.parse_img) - self.parse_file.pack(padx=5, pady=5) - self.parse_file.configure(state='disabled') - self.success_run = ctk.CTkLabel(self.content2, text='', width=180, height=25, - fg_color='white', corner_radius=5) - self.success_run.pack(padx=5, pady=10) - self.success_run.configure(state='disabled') - self.success_run.pack_forget() - - # A possible solution to shell opening, this surpresses opening cmd in exe file - def open_subprocess(): - cmd = '"%s"' % write_file_path - subprocess.call(cmd, shell=True) - - self.open_gen_file = ctk.CTkButton(self.content2, text=f'{lcat["open_gen_file"][self.lc]}', - #command=lambda: os.system('"%s"' % write_file_path), - command=open_subprocess, - compound='left', image=self.open_file_img) - self.open_gen_file.pack(padx=5, pady=5) - self.open_gen_file.pack_forget() - - # RUN GUI - self.gui.mainloop() - - def extend_gui_window(self, by=35): - self.dim_h += by - self.gui.geometry(f"250x{self.dim_h}") - - def open_readme(self): - # find the readme file against dynamic app location - file_path = os.path.join(application_path, f'{lcat["readme_file_path"][self.lc]}') - - readme = ctk.CTkToplevel() - readme.title('Readme') - readme.grab_set() - readme_text = ctk.CTkTextbox(readme, width=750, height=500) - readme_text.pack() - try: - with open(file_path,'r', encoding='utf-8') as readme_file: - text = readme_file.read() - readme_text.insert('0.0',text) - except IOError as error: - readme_text.insert('0.0',f'{lcat["readme_text"][self.lc]}\n{error}') - readme_text.configure(state='disabled') - - def set_language(self, language='HR'): - # Change language - lcat['language_setting'] = language - if language == 'HR': - messagebox.showinfo(message='Za promjenu jezika molim ponovno pokreni program!') - elif language == 'EN': - messagebox.showinfo(message='For changing the language please restart the program!') - # store language setting to a language catalog file - file_path = os.path.join(application_path, 'config_settings.json') - with open(file_path, 'w') as fileWriter: - json.dump(lcat, fileWriter, indent=4) - - def open_file_event(self, software): - global file_path, name_for_file - pt_filetype = (("Text file","*.txt*"), ("All files","*")) - td_filetype = (("CSV file","*.csv"), ("All files","*")) - if software == 'TopoDroid' or software == 'topodroidx': - file_path = ctk.filedialog.askopenfilename(initialdir='SurveyScraper', - title=f'{lcat["file_path_csv"][self.lc]}', filetypes = td_filetype) - elif software == 'PocketTopo': - file_path = ctk.filedialog.askopenfilename(initialdir='SurveyScraper', - title=f'{lcat["file_path_txt"][self.lc]}', filetypes = pt_filetype) - - if (not file_path.endswith('.csv') and (software == 'TopoDroid' or software == 'topodroidx')) or (not file_path.endswith('.txt') and software == 'PocketTopo'): - if not file_path == '': - messagebox.showerror('Error',f'{lcat["file_path_error"][self.lc]}') - file_path = None - if file_path: - file_base_name = os.path.basename(file_path) - name_for_file = os.path.splitext(file_base_name)[0] - if not self.opened_file.grid_info(): - self.extend_gui_window() - self.opened_file.grid() - self.opened_file.configure(text=file_base_name) - self.gui.update() - self.parse_file.configure(state='normal') - self.calc_magn_decl_btn.configure(state='normal') - - def calc_magn_decl(self): - MagneticDeclination_window(self) - - def parse_event(self, software): - global success - success = False - if self.create_write_file(): - if software == 'PocketTopo': - success = self.parse_pockettopo() - elif software == 'TopoDroid': - success = self.parse_topodroid() - if not self.success_run.winfo_ismapped(): # if it has not run, extend the window - self.extend_gui_window(70) - self.success_run.pack() - if success: - winsound.MessageBeep() - self.success_run.configure(text=f'{lcat["success_run_true"][self.lc]}') - self.open_gen_file.pack(padx=5, pady=5) - else: - self.success_run.configure(text=f'{lcat["success_run_false"][self.lc]}') - self.gui.update() - - # Save last used software - lcat['last_used_software'] = software - file_path = os.path.join(application_path, 'config_settings.json') - with open(file_path, 'w') as fileWriter: - json.dump(lcat, fileWriter, indent=4) - - def create_write_file(self): - global write_file_path - self.sign = self.survey_sign.get() - if self.sign == '': - messagebox.showwarning('Warning',f'{lcat["sign_warning"][self.lc]}') - write_file_path = ctk.filedialog.asksaveasfilename(initialdir='SurveyScraper', title=f'{lcat["write_file_path"][self.lc]}', - defaultextension=".csv", initialfile=f'{name_for_file}{lcat["sufix"][self.lc]}') - if write_file_path == '': - return False - try: - with open(write_file_path, 'w') as file: - file.write(f'{lcat["file.write"][self.lc]}\n') - return True - except IOError: - messagebox.showerror('Error',f'{lcat["IOError"][self.lc]}') - return False - - def parse_pockettopo(self): - with open(file_path,'r') as file: - for _ in range(6): - next(file) - shots = [] #a temporary shot list - for row in file: - row_data = re.sub(r'\[.\]','',row) - row_data = re.sub(r'<','',row_data) - shot = row_data.split() #a list of shot data - if len(shot) == 5: #filter for main shots - shots.append(shot) - if len(shots) == 2: - if shots[1][0] != shots[0][0] and shots[1][1] != shots[0][1]: #if the new shot name is different than the previous shot, save, else add to the list - if self.sign == '': - main_shot = f'{shots[0][0]},{shots[0][1]},{float(shots[0][2]):.3f},{float(shots[0][3]):.2f},{float(shots[0][4]):.2f}\n' - else: - main_shot = f'{self.sign}-{shots[0][0]},{self.sign}-{shots[0][1]},{float(shots[0][2]):.3f},{float(shots[0][3]):.2f},{float(shots[0][4]):.2f}\n' - with open(write_file_path, 'a') as file: - file.write(main_shot) - shots.pop(0) - if len(shots) == 3: - mean_len = (float(shots[0][2])+float(shots[1][2])+float(shots[2][2]))/3 - mean_dir = (float(shots[0][3])+float(shots[1][3])+float(shots[2][3]))/3 + self.md_val - mean_inc = (float(shots[0][4])+float(shots[1][4])+float(shots[2][4]))/3 - if self.sign == '': - main_shot = f'{shots[0][0]},{shots[0][1]},{mean_len:.3f},{mean_dir:.2f},{mean_inc:.2f}\n' - else: - main_shot = f'{self.sign}-{shots[0][0]},{self.sign}-{shots[0][1]},{mean_len:.3f},{mean_dir:.2f},{mean_inc:.2f}\n' - with open(write_file_path, 'a') as file: - file.write(main_shot) - shots.clear() - success = True - return success - - def parse_topodroid(self): - with open(file_path,'r') as file: - for _ in range(4): - next(file) - for row in file: - shot = row.split(',') - if shot[1] != '-': #filter for main shots - shot_from = shot[0][0:shot[0].find('@')] - shot_to = shot[1][0:shot[1].find('@')] - if self.sign == '': - main_shot = f'{shot_from},{shot_to},{shot[2]},{float(shot[3])+self.md_val},{shot[4]}\n' - else: - main_shot = f'{self.sign}-{shot_from},{self.sign}-{shot_to},{shot[2]},{float(shot[3])+self.md_val},{shot[4]}\n' - with open(write_file_path, 'a') as file: - file.write(main_shot) - success = True - return success - -class MagneticDeclination_window(): - def __init__(self, main_gui): - self.main_gui = main_gui - self.gui_md = ctk.CTkToplevel() - self.gui_md.title(f'{lcat["gui_md.title"][self.main_gui.lc]}') - self.gui_md.grab_set() - self.gui_md.resizable(False,False) - self.md_val = 0 - - # LOCATION FRAME - self.location_frame = ctk.CTkFrame(self.gui_md) - self.location_frame.grid(row=0, column=0, rowspan=4, padx=10, pady=5) - self.location_frm_label = ctk.CTkLabel(self.location_frame, text=f'{lcat["location_frm_label"][self.main_gui.lc]}:', font=('Roboto', 14)) - self.location_frm_label.grid(row=0, column=0, columnspan=3, padx=10, pady=5, sticky='ew') - self.location_label = ctk.CTkLabel(self.location_frame, text=f'{lcat["location_label"][self.main_gui.lc]}: ', font=('Roboto', 12)) - self.location_label.grid(row=1, column=0, padx=10, pady=5, sticky='e') - self.location_input = ctk.CTkEntry(self.location_frame, width=120, height=20, font=('Roboto', 12)) - self.location_input.grid(row=1, column=1, padx=10, pady=5) - self.get_coord_btn = ctk.CTkButton(self.location_frame, width=150, text=f'{lcat["get_coord_btn_label"][self.main_gui.lc]}', - command=lambda: self.get_location(self.location_input.get())) - self.get_coord_btn.grid(row=2, column=0, columnspan=3, padx=5, pady=5) - Hovertip(self.location_input,f'{lcat["hovertip"][self.main_gui.lc]}', hover_delay=500) - self.lat_label = ctk.CTkLabel(self.location_frame, text=f'{lcat["lat_label"][self.main_gui.lc]}: ', font=('Roboto', 12)) - self.lat_label.grid(row=3, column=0, padx=5, pady=5, sticky='e') - self.lat_input = ctk.CTkEntry(self.location_frame, width=100, height=20, font=('Roboto', 12)) - self.lat_input.grid(row=3, column=1, pady=5) - self.lat_N = ctk.CTkLabel(self.location_frame, text="N", font=('Roboto', 12)) - self.lat_N.grid(row=3, column=2, padx=5, pady=5, sticky='w') - self.lon_label = ctk.CTkLabel(self.location_frame, text=f'{lcat["lon_label"][self.main_gui.lc]}: ', font=('Roboto', 12)) - self.lon_label.grid(row=4, column=0, padx=5, pady=5, sticky='e') - self.lon_input = ctk.CTkEntry(self.location_frame, width=100, height=20, font=('Roboto', 12)) - self.lon_input.grid(row=4, column=1, pady=5) - self.lon_E = ctk.CTkLabel(self.location_frame, text="E", font=('Roboto', 12)) - self.lon_E.grid(row=4, column=2, padx=5, pady=5, sticky='w') - self.map = tkintermapview.TkinterMapView(self.location_frame, width=100, height=300, corner_radius=15) - self.map.grid(row=5, column=0, columnspan=3, padx=5, pady=5, sticky='ew') - self.map.set_position(float(lcat["map.set_position_x"][self.main_gui.lc]),float(lcat["map.set_position_y"][self.main_gui.lc])) - self.map.set_zoom(float(lcat["map.set_zoom"][self.main_gui.lc])) - - # MODEL FRAME - self.model_frame = ctk.CTkFrame(self.gui_md) - self.model_frame.grid(row=0, column=1, padx=10, pady=5, sticky='news') - self.model_label = ctk.CTkLabel(self.model_frame, text="Model:", font=('Roboto', 14)) - self.model_label.grid(row=0, column=0, columnspan=3, padx=10, pady=5, sticky='ew') - self.model = ctk.StringVar() - - current_date = datetime.date.today() - current_day = current_date.strftime('%d') - current_month = current_date.strftime('%m') - current_year = current_date.strftime('%Y') - def refresh_year(): - if self.model.get() == 'WMM': - year_range = [str(y) for y in range(2019,int(current_year)+2)] - elif self.model.get() == 'IGRF': - year_range = [str(y) for y in range(1590,int(current_year)+2)] - self.year_combo.configure(values=sorted(year_range, reverse=True)) - - self.model_wmm = ctk.CTkRadioButton(self.model_frame, text='WMM (2019-2024)', value='WMM', variable=self.model, command=refresh_year) - self.model_wmm.grid(row=1, column=1, padx=60, pady=5, sticky='w') - self.model_wmm.select() - self.model_igrf = ctk.CTkRadioButton(self.model_frame, text='IGRF (1590-2024)', value='IGRF', variable=self.model, command=refresh_year) - self.model_igrf.grid(row=2, column=1, padx=60, pady=5, sticky='w') - - # DATE FRAME - self.date_frame = ctk.CTkFrame(self.gui_md) - self.date_frame.grid(row=1, column=1, padx=10, pady=5, sticky='news') - self.date_frame.columnconfigure((0,1), weight=1) - self.date_label = ctk.CTkLabel(self.date_frame, text=f'{lcat["date_label"][self.main_gui.lc]}: ', font=('Roboto', 14)) - self.date_label.grid(row=0, column=0, columnspan=2, padx=10, pady=5, sticky='ew') - self.date_day = ctk.CTkLabel(self.date_frame, text=f'{lcat["date_day"][self.main_gui.lc]}:') - self.date_day.grid(row=1, column=0, padx=10, pady=5, sticky='e') - selected_day = ctk.StringVar(value=current_day) - self.day_combo = ctk.CTkComboBox(self.date_frame, width=60, height=20, values=[str(i) for i in range(1,32)], variable=selected_day) - self.day_combo.grid(row=1, column=1, padx=5, pady=5, sticky='w') - self.date_month = ctk.CTkLabel(self.date_frame, text=f'{lcat["date_month"][self.main_gui.lc]}:') - self.date_month.grid(row=2, column=0, padx=10, pady=5, sticky='e') - selected_month = ctk.StringVar(value=current_month) - self.month_combo = ctk.CTkComboBox(self.date_frame, width=60, height=20, values=[str(i) for i in range(1,13)], variable=selected_month) - self.month_combo.grid(row=2, column=1, padx=5, pady=5, sticky='w') - self.date_year = ctk.CTkLabel(self.date_frame, text=f'{lcat["date_year"][self.main_gui.lc]}:') - self.date_year.grid(row=3, column=0, padx=10, pady=5, sticky='e') - selected_year = ctk.StringVar(value=current_year) - year_range = [str(y) for y in range(2019,int(current_year)+2)] - self.year_combo = ctk.CTkComboBox(self.date_frame, width=60, height=20, values=sorted(year_range, reverse=True), variable=selected_year) - self.year_combo.grid(row=3, column=1, padx=5, pady=5, sticky='w') - - # CALCULATION FRAME - self.decl_frame = ctk.CTkFrame(self.gui_md) - self.decl_frame.grid(row=2, column=1, padx=10, pady=5, sticky='news') - self.decl_frame.columnconfigure(0, weight=1) - self.calc_md_btn = ctk.CTkButton(self.decl_frame, width=100, text=f'{lcat["calc_md_btn"][self.main_gui.lc]}', - command=lambda: self.get_md(self.model.get(),self.year_combo.get(),self.month_combo.get(),self.day_combo.get())) - self.calc_md_btn.grid(row=0, column=0, columnspan=3, padx=5, pady=10) - self.md_value_lbl = ctk.CTkLabel(self.decl_frame, text=f'{lcat["md_value_lbl"][self.main_gui.lc]}:') - self.md_value_lbl.grid(row=1, column=0, padx=5, pady=5) - self.md_value = ctk.CTkEntry(self.decl_frame, width=50, height=25, fg_color='white', corner_radius=5) - self.md_value.grid(row=1, column=1, padx=5, pady=5) - - # EXECUTE BUTTON - self.include_md_btn = ctk.CTkButton(self.gui_md, width=150, text=f'{lcat["include_md_btn"][self.main_gui.lc]}', command=self.apply_md) - self.include_md_btn.grid(row=3, column=1, padx=5, pady=5, sticky='ew') - self.include_md_btn.configure(state='disabled') - - def get_location(self, location): - global latitude, longitude - if location != '': - self.lat_input.delete(0,ctk.END) - self.lon_input.delete(0,ctk.END) - lat_lon_app = Retrieve_lat_lon(location) - latitude, longitude = lat_lon_app.retrieve_lat_lon() - self.lat_input.insert(0,f'{latitude:.4f}') - self.lon_input.insert(0,f'{longitude:.4f}') - self.map.set_address(location, marker=True) - self.map.set_zoom(13) - else: - messagebox.showerror('Error',f'{lcat["location_error"][self.main_gui.lc]}') - - def get_md(self, model, year, month, day): - try: - datetime.datetime(int(year),int(month),int(day)) - except ValueError: - messagebox.showerror('Error',f'{lcat["datetime.ValueError"][self.main_gui.lc]}') - if self.lat_input.get() == '' or self.lon_input.get() == '': - messagebox.showerror('Error',f'{lcat["lat_lon_input_error"][self.main_gui.lc]}') - else: - try: - magn_decl_app = Retrieve_magn_decl(float(self.lat_input.get()), float(self.lon_input.get()), model, year, month, day) - md_val = float(f'{magn_decl_app.retrieve_magn_decl():.3f}') - self.md_value.insert(0,f'{md_val}°') - self.md_value.configure(state='disabled') - self.main_gui.md_val = md_val #the characteristic of the main gui window (default value of 0) is now changed - self.include_md_btn.configure(state='normal') - except: - messagebox.showerror('Error',f'{lcat["lat_lon_input_error_manual"][self.main_gui.lc]}') - - def apply_md(self): - self.gui_md.destroy() - if not self.main_gui.show_md_value.grid_info(): - self.main_gui.extend_gui_window() - self.main_gui.show_md_value_lbl.grid() - self.main_gui.show_md_value.grid() - self.main_gui.show_md_value.insert(0,f'{self.main_gui.md_val}°') - -if __name__ == '__main__': - # Run config right at the start - lang = config() - SurveyScraper(lang) \ No newline at end of file diff --git a/surveyscraper_v2.exe b/surveyscraper_v3.exe similarity index 63% rename from surveyscraper_v2.exe rename to surveyscraper_v3.exe index 581ed66..5cee375 100644 Binary files a/surveyscraper_v2.exe and b/surveyscraper_v3.exe differ diff --git a/surveyscraper_v3.py b/surveyscraper_v3.py new file mode 100644 index 0000000..d0ad7e5 --- /dev/null +++ b/surveyscraper_v3.py @@ -0,0 +1,777 @@ +import re +import os +import csv +import sys +import json +import tkintermapview +import datetime +import winsound +import threading +import customtkinter as ctk +from PIL import Image +from tkinter import messagebox +from idlelib.tooltip import Hovertip +from mag_decl_webscrape import Retrieve_lat_lon, Retrieve_magn_decl +from speleoliti_handler import Speleoliti_online + +ctk.set_appearance_mode('light') +ctk.set_default_color_theme('green') + +def config(): + """Read configuration settings""" + global application_path + """Set the directory of the original file for a path to any other file""" + if getattr(sys,'frozen', False): #check if the app runs as a script or as a frozen exe file + application_path = os.path.dirname(sys.executable) + elif __file__: + application_path = os.path.dirname(__file__) + + """Read language catalog""" + global lcat + file_path = os.path.join(application_path, 'config_settings.json') + try: + with open(file_path,'r', encoding='utf-8') as file: + lcat = json.load(file) + return lcat['language_setting'], lcat['last_used_software'] + except Exception as ex: + messagebox.showerror('Error',f'There was an error while reading the file: {ex}') + +class SurveyScraper(): + def __init__(self, lc, last_used): + """Initialise GUI screen""" + + self.gui = ctk.CTk() + self.gui.title('SurveyScraper') + self.gui.geometry(f"540x525+200+200") # height x width + self.gui.resizable(False,False) + self.gui.columnconfigure(0, weight=6) + self.gui.columnconfigure(1, weight=1) + self.md_val = 0 # default value of mag decl + self.speleoliti_app = None + self.survey_date = None + self.cave_name = None + self.cave_survey_opened = False + self.original_angles = [] + self.offline = False + if lc == 'HR': + self.lc = 0 + elif lc == 'EN': + self.lc = 1 + self.last_used_software = last_used + + def resource_path(relative_path): + """ Get absolute path to resource, works for dev and for PyInstaller.""" + try: + # PyInstaller creates a temp folder and stores path in _MEIPASS + base_path = sys._MEIPASS + except Exception: + #base_path = os.path.abspath(".") + base_path = os.path.dirname(__file__) + return os.path.join(base_path, relative_path) + # Set path to a current cave survey data + self.survey_data_file_path = resource_path('survey_data.json') + + # TITLE + self.gui_title = ctk.CTkLabel(self.gui, text='SurveyScraper v3.0', font=('Roboto', 18)) + self.gui_title.grid(row=0, column=0, padx=10, pady=5, sticky='w') + + # self.use_offline_var = ctk.StringVar(value="off") + # self.use_offline = ctk.CTkSwitch(self.gui, text="Offline", font=("Roboto",14), + # variable=self.use_offline_var, onvalue="on", offvalue="off") + # self.use_offline.grid(row=0, column=1, padx=5, pady=5, sticky='e') + # self.use_offline.deselect() + + self.open_speleoliti_btn = ctk.CTkButton(self.gui, text=f'Speleoliti Online', width=20, font=('Roboto', 15), + command=self.open_speleoliti) + self.open_speleoliti_btn.grid(row=0, column=2, padx=5, pady=5, sticky='e') + self.lang_var = ctk.StringVar(self.gui) + self.lang_var.set(lc) + language_choice = ctk.CTkOptionMenu(self.gui, values=['HR'], variable=self.lang_var, anchor='center', + font=('Roboto', 15), width=20, command=self.set_language) + language_choice.grid(row=0, column=3, padx=0, pady=5, sticky='e') + + # TABVIEW + self.tabview = ctk.CTkTabview(self.gui) + self.tabview.grid(row=1, column=0, columnspan=4, sticky='ew') + self.tabview.add('Main') + self.tabview.add('MagDec') + self.tabview.add('Help') + self.tabview._segmented_button.grid(sticky='w') + + ############################## + # MAIN TAB FRAME + self.main_tab_frame = ctk.CTkFrame(self.tabview.tab('Main')) + self.main_tab_frame.pack(padx=5, pady=5, expand=True, fill="both") + # + # IMPORT FILE SUBFRAME - LEFT + self.import_frm = ctk.CTkFrame(self.main_tab_frame) + self.import_frm.grid(row=0, column=0, padx=5, pady=5, sticky='nsew') + # + self.import_frm_lbl = ctk.CTkLabel(self.import_frm, text=f'Uvoz:', font=("Roboto", 14), fg_color='darkgray', corner_radius=5) + self.import_frm_lbl.grid(row=0, column=0, columnspan=2, padx=5, pady=5, sticky='ew') + self.open_file_img = ctk.CTkImage(Image.open(resource_path(os.path.join('img','open_file.png'))), size=(15,15)) + self.open_file = ctk.CTkButton(self.import_frm, text=f'Otvori vlakove', + command=self.open_file_event, compound='left', image=self.open_file_img) + self.open_file.grid(row=1, column=0, padx=(5,5), pady=10, columnspan=2) + + self.opened_file = ctk.CTkLabel(self.import_frm, text='', width=200, height=25, fg_color='white', corner_radius=5) + self.opened_file.grid(row=2, column=0, padx=5, pady=5, columnspan=2) + self.opened_file.configure(state='disabled') + + self.survey_sign_lbl = ctk.CTkLabel(self.import_frm, text='Prefiks točaka') + self.survey_sign_lbl.grid(row=3, column=0, padx=5, pady=5, sticky='e') + self.shot_prefix_fld = ctk.CTkEntry(self.import_frm, width=50, height=25) + self.shot_prefix_fld.grid(row=3, column=1, padx=5, pady=5, sticky='w') + self.calc_md_img = ctk.CTkImage(Image.open(resource_path('img/compass.png')), size=(15,15)) + self.show_md_value_lbl = ctk.CTkLabel(self.import_frm, text=f'{lcat["show_md_value_lbl"][self.lc]}:') + self.show_md_value_lbl.grid(row=4, column=0, padx=5, pady=5, sticky='e') + self.show_md_value = ctk.CTkEntry(self.import_frm, width=50, height=25, fg_color='white', corner_radius=5) + self.show_md_value.grid(row=4, column=1, padx=5, pady=5, sticky='w') + + self.parse_img = ctk.CTkImage(Image.open(resource_path('img/generate.png')), size=(15,15)) + self.apply_fix_md_btn = ctk.CTkButton(self.import_frm, text='Pohrani postavke', command=self.save_json, + compound='left', image=self.parse_img) + self.apply_fix_md_btn.grid(row=5, column=0, padx=(5,5), pady=5, columnspan=2) + self.apply_fix_md_btn.configure(state='disabled') + + # EXPORT FILE SUBFRAME - LEFT + self.export_frm = ctk.CTkFrame(self.main_tab_frame) + self.export_frm.grid(row=1, column=0, padx=5, pady=5, sticky='nsew') + # + self.export_frm_lbl = ctk.CTkLabel(self.export_frm, text=f'Izvoz:', font=("Roboto",14), fg_color='darkgray', corner_radius=5) + self.export_frm_lbl.pack(padx=5, pady=5, fill='both') + # self.save_json_file = ctk.CTkButton(self.export_frm, text='Pohrani', command=self.save_json, + # compound='left', image=self.parse_img) + #self.save_json_file.grid(row=1, column=0, padx=5, pady=5, sticky='ew') + # self.save_json_file.pack(padx=5, pady=5) + # self.save_json_file.configure(state='disabled') + self.save_csv_file = ctk.CTkButton(self.export_frm, text='Izvezi u CSV', command=self.store_to_csv, + compound='left', image=self.parse_img) + self.save_csv_file.pack(padx=5, pady=5) + self.save_csv_file.configure(state='disabled') + self.export_original_angle_var = ctk.StringVar(value="on") + self.export_original_angle = ctk.CTkSwitch(self.export_frm, text="Dodaj stupac izvornih azimuta", font=("Roboto",10), + variable=self.export_original_angle_var, onvalue="on", offvalue="off") + self.export_original_angle.pack(padx=5, pady=5) + self.export_original_angle.deselect() + self.export_original_angle.configure(state='disabled') + # self.open_gen_file = ctk.CTkButton(self.export_frm, text=f'{lcat["open_gen_file"][self.lc]}', + # #command=lambda: os.system('"%s"' % write_csv_file_path), + # command=open_subprocess, + # compound='left', image=self.open_file_img) + # self.open_gen_file.grid(row=5, column=0, padx=5, pady=5) + # self.open_gen_file.configure(state='disabled') + self.success_run = ctk.CTkLabel(self.export_frm, text='', height=25, width=200, fg_color='white', corner_radius=5) + #self.success_run.grid(row=3, column=0, padx=5, pady=5, sticky='ew') + self.success_run.pack(padx=5, pady=5, fill='both') + self.success_run.configure(state='disabled') + + ############################## + # MAIN TAB FRAME - RIGHT + # CAVE DATA SUBFRAME + mtf_entry_width = 120 + self.cave_data_frame = ctk.CTkFrame(self.main_tab_frame, height=450) + self.cave_data_frame.grid(row=0, rowspan=2, column=1, padx=5, pady=5, sticky='nsew') + # + # Cave name + self.cave_name_lbl = ctk.CTkLabel(self.cave_data_frame, text=f'Ime objekta: ', font=("Roboto", 14), + fg_color='darkgray', corner_radius=5) + self.cave_name_lbl.grid(row=0, column=0, padx=5, pady=5, ipadx=35, sticky='e') + self.cave_name_fld = ctk.CTkEntry(self.cave_data_frame, width=mtf_entry_width, height=25, fg_color='white', corner_radius=5) + self.cave_name_fld.grid(row=0, column=1, padx=5, pady=5, sticky='w') + # X + self.X_lbl = ctk.CTkLabel(self.cave_data_frame, text='Koordinata X: ') + self.X_lbl.grid(row=1, column=0, padx=5, pady=5, sticky='e') + self.X = ctk.CTkEntry(self.cave_data_frame, width=mtf_entry_width, height=25, fg_color='white', corner_radius=5) + self.X.grid(row=1, column=1, padx=5, pady=5, sticky='w') + # Y + self.Y_lbl = ctk.CTkLabel(self.cave_data_frame, text='Koordinata Y: ') + self.Y_lbl.grid(row=2, column=0, padx=5, pady=5, sticky='e') + self.Y = ctk.CTkEntry(self.cave_data_frame, width=mtf_entry_width, height=25, fg_color='white', corner_radius=5) + self.Y.grid(row=2, column=1, padx=5, pady=5, sticky='w') + # Z + self.Z_lbl = ctk.CTkLabel(self.cave_data_frame, text='Nadmorska visina Z: ') + self.Z_lbl.grid(row=3, column=0, padx=5, pady=5, sticky='e') + self.Z = ctk.CTkEntry(self.cave_data_frame, width=mtf_entry_width, height=25, fg_color='white', corner_radius=5) + self.Z.grid(row=3, column=1, padx=5, pady=5, sticky='w') + # File Name + self.file_name_lbl = ctk.CTkLabel(self.cave_data_frame, text='Ime datoteke za pohranu: ') + self.file_name_lbl.grid(row=4, column=0, columnspan=2, padx=5, pady=5) + self.file_name_fld = ctk.CTkEntry(self.cave_data_frame, width=mtf_entry_width+30, height=25) + self.file_name_fld.grid(row=5, column=0, columnspan=2, padx=5, pady=5, sticky='ew') + # Description + # self.description_lbl = ctk.CTkLabel(self.cave_data_frame, text='Opis: ') + # self.description_lbl.grid(row=5, column=0, padx=5, pady=5, sticky='e') + # self.description = ctk.CTkEntry(self.cave_data_frame, width=mtf_entry_width, height=25, fg_color='white', corner_radius=5) + # self.description.grid(row=5, column=1, padx=5, pady=5, sticky='w') + + # Fixed point + self.fixed_station_lbl = ctk.CTkLabel(self.cave_data_frame, text='Fiksna točka: ') + self.fixed_station_lbl.grid(row=6, column=0, padx=5, pady=5, sticky='e') + self.fixed_station_fld = ctk.CTkEntry(self.cave_data_frame, width=mtf_entry_width, height=25, fg_color='white', corner_radius=5) + self.fixed_station_fld.grid(row=6, column=1, padx=5, pady=5, sticky='w') + # Poly Length + self.poly_length_lbl = ctk.CTkLabel(self.cave_data_frame, text='Poligonalna duljina: ') + self.poly_length_lbl.grid(row=7, column=0, padx=5, pady=5, sticky='e') + self.poly_length_fld = ctk.CTkLabel(self.cave_data_frame, width=50, height=25, text='') + self.poly_length_fld.grid(row=7, column=1, padx=5, pady=5, sticky='w') + # Horizontal Length + self.hor_length_lbl = ctk.CTkLabel(self.cave_data_frame, text='Horizontalna duljina: ') + self.hor_length_lbl.grid(row=8, column=0, padx=5, pady=5, sticky='e') + self.hor_length_fld = ctk.CTkLabel(self.cave_data_frame, width=50, height=25, text='') + self.hor_length_fld.grid(row=8, column=1, padx=5, pady=5, sticky='w') + # Elevation + self.elevation_lbl = ctk.CTkLabel(self.cave_data_frame, text='Visinska razlika: ') + self.elevation_lbl.grid(row=9, column=0, padx=5, pady=5, sticky='e') + self.elevation_fld = ctk.CTkLabel(self.cave_data_frame, width=50, height=25, text='') + self.elevation_fld.grid(row=9, column=1, padx=5, pady=5, sticky='w') + # Depth + self.depth_lbl = ctk.CTkLabel(self.cave_data_frame, text='Dubina od fiksne točke: ') + self.depth_lbl.grid(row=10, column=0, padx=5, pady=5, sticky='e') + self.depth_fld = ctk.CTkLabel(self.cave_data_frame, width=50, height=25, text='') + self.depth_fld.grid(row=10, column=1, padx=5, pady=5, sticky='w') + + ############################## + # MAG DECL TAB FRAME + self.mag_decl_tab_frame = ctk.CTkFrame(self.tabview.tab('MagDec')) + self.mag_decl_tab_frame.pack(padx=5, pady=5, expand=True, fill="both") + # + # LOCATION SUBFRAME - LEFT + self.location_frame = ctk.CTkFrame(self.mag_decl_tab_frame) + self.location_frame.grid(row=0, column=0, rowspan=4, padx=(5,5), pady=5, sticky='nsew') + # + self.location_frm_label = ctk.CTkLabel(self.location_frame, text=f'{lcat["location_frm_label"][self.lc]}:', font=('Roboto', 14)) + self.location_frm_label.grid(row=0, column=0, columnspan=3, padx=10, pady=(5,0), sticky='ew') + self.location_label = ctk.CTkLabel(self.location_frame, text=f'{lcat["location_label"][self.lc]}: ', font=('Roboto', 12)) + self.location_label.grid(row=1, column=0, padx=10, pady=(5,0), sticky='e') + self.location_input = ctk.CTkEntry(self.location_frame, width=150, height=20, font=('Roboto', 12)) + self.location_input.grid(row=1, column=1, padx=10, pady=(5,0)) + self.get_coord_btn = ctk.CTkButton(self.location_frame, width=150, text=f'{lcat["get_coord_btn_label"][self.lc]}', + command=lambda: self.get_location(self.location_input.get())) + self.get_coord_btn.grid(row=2, column=0, columnspan=3, padx=5, pady=5) + Hovertip(self.location_input,f'{lcat["hovertip"][self.lc]}', hover_delay=500) + self.lat_label = ctk.CTkLabel(self.location_frame, text=f'{lcat["lat_label"][self.lc]}: ', font=('Roboto', 12)) + self.lat_label.grid(row=3, column=0, padx=(5,0), pady=(5,0), sticky='e') + self.lat_input = ctk.CTkEntry(self.location_frame, width=100, height=20, font=('Roboto', 12)) + self.lat_input.grid(row=3, column=1, padx=0, pady=(5,0)) + self.lat_N = ctk.CTkLabel(self.location_frame, text="N", font=('Roboto', 12)) + self.lat_N.grid(row=3, column=2, padx=(5,10), pady=(5,0), sticky='ew') + self.lon_label = ctk.CTkLabel(self.location_frame, text=f'{lcat["lon_label"][self.lc]}: ', font=('Roboto', 12)) + self.lon_label.grid(row=4, column=0, padx=(5,0), pady=(5,0), sticky='e') + self.lon_input = ctk.CTkEntry(self.location_frame, width=100, height=20, font=('Roboto', 12)) + self.lon_input.grid(row=4, column=1, padx=0, pady=(5,0)) + self.lon_E = ctk.CTkLabel(self.location_frame, text="E", font=('Roboto', 12)) + self.lon_E.grid(row=4, column=2, padx=(5,10), pady=(5,0), sticky='ew') + + self.map = tkintermapview.TkinterMapView(self.location_frame, width=150, height=300, corner_radius=15) + self.map.grid(row=5, column=0, columnspan=3, padx=5, pady=5, sticky='ew') + self.map.set_position(float(lcat["map.set_position_x"][self.lc]), float(lcat["map.set_position_y"][self.lc])) + self.map.set_zoom(int(lcat["map.set_zoom"][self.lc])) + + # MODEL SUBFRAME - RIGHT + self.model_frame = ctk.CTkFrame(self.mag_decl_tab_frame) + self.model_frame.grid(row=0, column=1, padx=(0,5), pady=5, sticky='nsew') + self.model_frame.columnconfigure((0,1), weight=1) + # + self.model_label = ctk.CTkLabel(self.model_frame, text="Model:", font=('Roboto', 14)) + self.model_label.grid(row=0, column=0, padx=10, pady=5, sticky='ew') + self.model = ctk.StringVar() + current_date = datetime.date.today() + current_day = current_date.strftime('%d') + current_month = current_date.strftime('%m') + current_year = current_date.strftime('%Y') + def refresh_year(): + if self.model.get() == 'WMM': + year_range = [str(y) for y in range(2019,int(current_year)+2)] + elif self.model.get() == 'IGRF': + year_range = [str(y) for y in range(1590,int(current_year)+2)] + self.year_combo.configure(values=sorted(year_range, reverse=True)) + + self.model_wmm = ctk.CTkRadioButton(self.model_frame, text='WMM (2019-2024)', value='WMM', variable=self.model, command=refresh_year) + self.model_wmm.grid(row=1, column=0, padx=30, pady=5, sticky='w') + self.model_wmm.select() + self.model_igrf = ctk.CTkRadioButton(self.model_frame, text='IGRF (1590-2024)', value='IGRF', variable=self.model, command=refresh_year) + self.model_igrf.grid(row=2, column=0, padx=30, pady=5, sticky='w') + + # DATE SUBFRAME + self.date_frame = ctk.CTkFrame(self.mag_decl_tab_frame) + self.date_frame.grid(row=1, column=1, padx=(0,5), pady=5, sticky='nsew') + self.date_frame.columnconfigure((0,1), weight=1) + # + self.date_label = ctk.CTkLabel(self.date_frame, text=f'{lcat["date_label"][self.lc]}: ', font=('Roboto', 14)) + self.date_label.grid(row=0, column=0, columnspan=2, padx=10, pady=5, sticky='ew') + self.date_day = ctk.CTkLabel(self.date_frame, text=f'{lcat["date_day"][self.lc]}:') + self.date_day.grid(row=1, column=0, padx=10, pady=5, sticky='e') + self.selected_day = ctk.StringVar(value=current_day) + self.day_combo = ctk.CTkComboBox(self.date_frame, width=60, height=20, values=[str(i) for i in range(1,32)], variable=self.selected_day) + self.day_combo.grid(row=1, column=1, padx=5, pady=5, sticky='w') + self.date_month = ctk.CTkLabel(self.date_frame, text=f'{lcat["date_month"][self.lc]}:') + self.date_month.grid(row=2, column=0, padx=10, pady=5, sticky='e') + self.selected_month = ctk.StringVar(value=current_month) + self.month_combo = ctk.CTkComboBox(self.date_frame, width=60, height=20, values=[str(i) for i in range(1,13)], variable=self.selected_month) + self.month_combo.grid(row=2, column=1, padx=5, pady=5, sticky='w') + self.date_year = ctk.CTkLabel(self.date_frame, text=f'{lcat["date_year"][self.lc]}:') + self.date_year.grid(row=3, column=0, padx=10, pady=5, sticky='e') + self.selected_year = ctk.StringVar(value=current_year) + year_range = [str(y) for y in range(2019,int(current_year)+2)] + self.year_combo = ctk.CTkComboBox(self.date_frame, width=60, height=20, values=sorted(year_range, reverse=True), variable=self.selected_year) + self.year_combo.grid(row=3, column=1, padx=5, pady=5, sticky='w') + + # CALCULATION SUBFRAME + self.decl_frame = ctk.CTkFrame(self.mag_decl_tab_frame) + self.decl_frame.grid(row=2, column=1, padx=(0,5), pady=5, sticky='nsew') + self.decl_frame.columnconfigure((0,1), weight=1) + # + self.calc_md_btn = ctk.CTkButton(self.decl_frame, width=125, text=f'{lcat["calc_md_btn"][self.lc]}', + command=lambda: self.get_md(self.model.get(),self.year_combo.get(),self.month_combo.get(),self.day_combo.get())) + self.calc_md_btn.grid(row=0, column=0, columnspan=3, padx=5, pady=10) + self.md_value_lbl = ctk.CTkLabel(self.decl_frame, text=f'{lcat["md_value_lbl"][self.lc]}:') + self.md_value_lbl.grid(row=1, column=0, padx=5, pady=5, sticky='e') + self.md_value = ctk.CTkEntry(self.decl_frame, width=50, height=25, fg_color='white', corner_radius=5) + self.md_value.grid(row=1, column=1, padx=5, pady=5, sticky='w') + self.md_value.configure(state='disabled') + + ############################## + # HELP TAB FRAME + self.help_tab_frame = ctk.CTkFrame(self.tabview.tab('Help')) + self.help_tab_frame.pack(padx=5, pady=5, expand=True, fill="both") + + # find the readme file against dynamic app location + readme_file_path = os.path.join(application_path, f'{lcat["readme_file_path"][self.lc]}') + readme_text = ctk.CTkTextbox(self.help_tab_frame, height=425, wrap='word') + readme_text.pack(padx=5, pady=5, expand=True, fill="both") + try: + with open(readme_file_path,'r', encoding='utf-8') as readme_file: + text = readme_file.read() + #processed_text = text.replace('\n', ' ') + readme_text.insert('0.0', text) + except IOError as error: + readme_text.insert('0.0',f'{lcat["readme_text"][self.lc]}\n{error}') + readme_text.configure(state='disabled') + + # RUN GUI + self.gui.mainloop() + + # MAIN MENU BUTTONS + def open_speleoliti(self): + # open loading window in GUI and begin process + self.loading_window = ctk.CTkToplevel(self.gui) + self.loading_window.title('SurveyScraper') + self.loading_window.attributes('-topmost', True) + self.gui.eval(f'tk::PlaceWindow {str(self.loading_window)} center') + loading_label = ctk.CTkLabel(self.loading_window, text="Otvaram Speleoliti online...", font=("Roboto",14)) + loading_label.pack(padx=50, pady=(25,25), expand=True, fill="x") + self.gui.update() + try: + self.speleoliti_app = Speleoliti_online(headless=False, survey_path=self.survey_data_file_path) + if self.speleoliti_app.online: + self.speleoliti_app.driver.get('chrome://settings/') + self.speleoliti_app.driver.execute_script('chrome.settingsPrivate.setDefaultZoom(1.5);') # Set zoom level to 150% + if self.cave_survey_opened: # If there is a cave survey opened, open it + self.speleoliti_app.open_object() + self.speleoliti_app.update_fixed_station(self.fixed_station) + else: + self.speleoliti_app.open_empty_object() + self.speleoliti_app.restore_window() + else: + messagebox.showerror('Error', f'Nije moguće otvoriti Speleoliti Online bez internet veze!') + self.loading_window.destroy() + except Exception as e: + messagebox.showerror('Error', f'Pogreška prilikom otvaranja Speleoliti online\n\n{e}\n Molim pokušaj ponovo kasnije.') + + def set_language(self, language='HR'): + # Change language + lcat['language_setting'] = language + if language == 'HR': + messagebox.showinfo(message='Za promjenu jezika molim ponovno pokreni program!') + elif language == 'EN': + messagebox.showinfo(message='For changing the language please restart the program!') + # store language setting to a language catalog file + file_path = os.path.join(application_path, 'config_settings.json') + with open(file_path, 'w') as fileWriter: + json.dump(lcat, fileWriter, indent=4) + + # CAVE SURVEY FILE IMPORT + def open_file_event(self): + """Open raw cave survey data file csv or txt, create json and execute parsing""" + + # Suggest the recent software to open file from + filetypes = (("CSV file","*.csv"), ("Text file","*.txt*"), ("Walls file","*.srv*"), ("All files","*")) + software_to_file = { + "TopoDroid":"CSV file", + "PocketTopo":"Text file", + "Qave":"Walls file" + } + filetypes = sorted(filetypes, key=lambda ft: ft[0] != software_to_file[self.last_used_software]) + self.file_path = ctk.filedialog.askopenfilename(initialdir='SurveyScraper', + title=f'{lcat["file_path"][self.lc]}', filetypes = filetypes) + if self.file_path.endswith('.csv'): + self.software = 'TopoDroid' + elif self.file_path.endswith('.txt'): + self.software = 'PocketTopo' + elif self.file_path.endswith('.srv'): + self.software = 'Qave' + else: + #messagebox.showerror('Error',f'{lcat["file_path_error"][self.lc]}') + self.file_path = None + # Continue with reading and processing the file + if self.file_path: + # Set the last used software and store to config file + lcat['last_used_software'] = self.software + file_path = os.path.join(application_path, 'config_settings.json') + with open(file_path, 'w') as fileWriter: + json.dump(lcat, fileWriter, indent=4) + # Make changes if the file opens correctly + file_base_name = os.path.basename(self.file_path) + self.suggested_name_for_file = os.path.splitext(file_base_name)[0] + f'{lcat["sufix"][self.lc]}' + self.cave_survey_json_data = {"fix": "","x": "","y": "","z": "","dcl": "","name": "","descr": "","viz": ["null"]} + self.create_json() # create empty json file + self.parse_event(self.software) + # continue if parsed successfully + if parsed: + self.cave_survey_opened = True + # Update GUI + if self.cave_name: + self.cave_name_fld.delete(0, ctk.END) # overwrite + self.cave_name_fld.insert(0, self.cave_name) #missing for pockettopo and qave + if self.file_name_fld: + self.file_name_fld.delete(0, ctk.END) # overwrite + self.file_name_fld.insert(0, f'{self.suggested_name_for_file}') + if not self.offline: + self.fixed_station_fld.delete(0, ctk.END) + self.fixed_station_fld.insert(0, self.fixed_station) + self.poly_length_fld.configure(text=f'{float(self.poly_length):.1f} m') + self.hor_length_fld.configure(text=f'{float(self.hor_length):.1f} m') + self.elevation_fld.configure(text=f'{float(self.elevation):.1f} m') + self.depth_fld.configure(text=f'{float(self.depth):.1f} m') + if self.survey_date: + self.selected_day.set(str(self.survey_date.day)) + self.selected_month.set(str(self.survey_date.month)) + self.selected_year.set(str(self.survey_date.year)) + self.opened_file.configure(text=file_base_name) + self.apply_fix_md_btn.configure(state='normal') + self.save_csv_file.configure(state='normal') + self.gui.update() + else: + messagebox.showerror('Error','Error due to failed cave survey file parsing!') + + # APPLY CHANGES + def save_json(self): + """Apply prefix and mag decl to cave_survey_json_data and save""" + + self.cave_survey_json_data['name'] = self.cave_name_fld.get() + self.cave_survey_json_data['x'] = self.X.get() + self.cave_survey_json_data['y'] = self.Y.get() + self.cave_survey_json_data['z'] = self.Z.get() + # save prefix + if not hasattr(self, 'original_fixed_station_name'): + self.original_fixed_station_name = self.fixed_station_fld.get() + new_shot_prefix = self.shot_prefix_fld.get() + self.cave_survey_json_data['descr'] = new_shot_prefix + if self.original_fixed_station_name: + new_fixed_station_name = f"{new_shot_prefix}{self.original_fixed_station_name}" + self.cave_survey_json_data['fix'] = new_fixed_station_name + # save md val + if not hasattr(self, 'original_shots'): + self.original_shots = [{'t1': shot['t1'], 't2': shot['t2'], 'a': shot['a']} for shot in self.cave_survey_json_data['viz'][1:]] + if self.show_md_value.get(): + try: + md_value_str = self.show_md_value.get().split('°')[0] + self.md_val = float(md_value_str.replace(',', '.')) % 360 + if float(md_value_str) >= 360: + self.show_md_value.delete(0, 'end') + self.show_md_value.insert(0,f'{self.md_val}°') + except ValueError: + messagebox.showerror('Error',f'Pogrešan unos magnetske deklinacije!') + self.cave_survey_json_data['dcl'] = self.md_val + self.export_original_angle.configure(state='normal') # enable option for storing original angles if md is set + for original_shot, shot in zip(self.original_shots, self.cave_survey_json_data['viz'][1:]): + shot['t1'] = f"{new_shot_prefix}{original_shot['t1']}" + shot['t2'] = f"{new_shot_prefix}{original_shot['t2']}" + shot['a'] = round((float(original_shot['a']) + self.md_val) % 360,2) + shot['l'] = round(float(shot['l']),3) + shot['f'] = round(float(shot['f']),2) + # save to json + with open(write_json_file_path, 'w') as file: + json.dump(self.cave_survey_json_data, file, indent=4) + # update gui + if self.fixed_station_fld.get(): + self.fixed_station_fld.delete(0, ctk.END) + self.fixed_station_fld.insert(0, new_fixed_station_name) + self.success_run.configure(text='Pohranjeno!') + winsound.MessageBeep() + self.gui.update() + + # CAVE SURVEY FILE EXPORT + def create_json(self): + """Create empty cave survey data JSON file""" + global write_json_file_path + write_json_file_path = os.path.join(application_path, 'survey_data.json') + with open(write_json_file_path, 'w') as file: + json.dump(self.cave_survey_json_data, file, indent=4) + + def store_to_csv(self): + """Store cave survey data to CSV file""" + + shot_prefix = self.cave_survey_json_data['descr'] + if hasattr(self,'file_name_fld'): + self.suggested_name_for_file = self.file_name_fld.get() + if shot_prefix == '' and self.software == 'PocketTopo': # raise warning if exporting shots from Speleoliti without prefix + messagebox.showwarning('Warning',f'{lcat["sign_warning"][self.lc]}') + write_csv_file_path = ctk.filedialog.asksaveasfilename(initialdir='SurveyScraper', title=f'{lcat["write_csv_file_path"][self.lc]}', + defaultextension=".csv", initialfile=f'{self.suggested_name_for_file}') + try: + with open(write_csv_file_path, 'w', newline='') as csv_file: + if not self.offline: + description = (f"Ime objekta:,{self.cave_survey_json_data['name']}\n" + f"X:,{self.cave_survey_json_data['x']}\n" + f"Y:,{self.cave_survey_json_data['y']}\n" + f"Z:,{self.cave_survey_json_data['z']}\n" + f"Fiksna točka:,{self.cave_survey_json_data['fix']}\n" + f"Magn. deklinacija:,{self.cave_survey_json_data['dcl']}\n" + f"Poligonalna duljina:,{float(self.poly_length):.1f}\n" + f"Horizontalna duljina:,{float(self.hor_length):.1f}\n" + f"Visinska razlika:,{float(self.elevation):.1f}\n" + f"Dubina od fiksne točke:,{float(self.depth):.1f}\n") + else: + description = (f"Ime objekta:,{self.cave_survey_json_data['name']}\n" + f"X:,{self.cave_survey_json_data['x']}\n" + f"Y:,{self.cave_survey_json_data['y']}\n" + f"Z:,{self.cave_survey_json_data['z']}\n" + f"Fiksna točka:,{self.cave_survey_json_data['fix']}\n") + csv_file.write(description) + fieldnames = list(self.cave_survey_json_data['viz'][1].keys()) + if self.export_original_angle_var.get() == 'on': + fieldnames += ['a_original'] + shots_to_process = zip(self.cave_survey_json_data['viz'][1:], self.original_shots) + else: + # Create an iterator of shots without pairing with original_shots + shots_to_process = ((shot, None) for shot in self.cave_survey_json_data['viz'][1:]) + writer = csv.DictWriter(csv_file, fieldnames=fieldnames) + writer.writeheader() + for shot, original_shot in shots_to_process: + shot_copy = shot.copy() + if original_shot is not None: + shot_copy['a_original'] = round(float(original_shot['a']),2) + writer.writerow(shot_copy) + fieldnames = [] + self.success_run.configure(text='CSV pohranjen!') + winsound.MessageBeep() + except IOError: + messagebox.showerror('Error',f'{lcat["IOError"][self.lc]}') + + # SPELEOLITI CALCULATION FUNCTION + def run_speleoliti_calculation(self): + #try: + self.speleoliti_app_headless = Speleoliti_online(headless=True, survey_path=self.survey_data_file_path) + if self.speleoliti_app_headless.online: + # refresh toplevel window GUI + opening_speleoliti_label = ctk.CTkLabel(self.loading_window, text="Povezujem se sa Speleoliti online...") + opening_speleoliti_label.pack(padx=10, pady=(0,25), fill="both") + self.gui.update() + # continue with Speleoliti process + self.speleoliti_app_headless.open_object() + self.fixed_station = self.speleoliti_app_headless.find_highest_point() + self.speleoliti_app_headless.update_fixed_station(self.fixed_station) + self.poly_length, self.hor_length, self.elevation, self.depth = self.speleoliti_app_headless.retrieve_cave_data() + self.speleoliti_app_headless.close_driver() + # except Exception as e: + # messagebox.showerror('Error', f'Pogreška prilikom obrade u Speleolitima!\n\n{e}') + self.cave_survey_json_data['fix'] = self.fixed_station # set fixed station by default + self.cave_survey_json_data['name'] = self.cave_name + else: + self.offline = True + messagebox.showwarning('Warning','Preskačem obradu u Speleoliti Online jer nema internet veze!') + + # CAVE SURVEY FILE PARSE FUNCTIONS + def parse_event(self, software): + """Parse event executes cave survey file parsing, speleoliti calculation and saving into json""" + global parsed + parsed = False + # load processing window in GUI and begin process + self.loading_window = ctk.CTkToplevel(self.gui) + self.loading_window.title('SurveyScraper') + self.loading_window.attributes('-topmost', True) + self.gui.eval(f'tk::PlaceWindow {str(self.loading_window)} center') + #self.loading_window.eval('tk::PlaceWindow . center') + loading_label = ctk.CTkLabel(self.loading_window, text="Obrađujem, molim pričekaj...") + loading_label.pack(padx=50, pady=(25,25), expand=True, fill="x") + self.gui.update() + # start parsing + if software == 'TopoDroid': + parsed = self.parse_topodroid() + elif software == 'Qave': + parsed = self.parse_qave() + elif software == 'PocketTopo': + parsed = self.parse_pockettopo() + if parsed: + try: + threading.Thread(target=self.run_speleoliti_calculation()).start() + winsound.MessageBeep() + self.success_run.configure(text='Uspješan uvoz i obrada!') + except Exception as e: + parsed = False + self.success_run.configure(text='Neuspješan uvoz i obrada!') + self.loading_window.destroy() + self.gui.update() + + def parse_pockettopo(self): + with open(self.file_path,'r') as file: + for _ in range(6): + next(file) + three_shots = [] #a temporary shot list + for row in file: + row_data = re.sub(r'\[.\]','',row) # remove brackets + row_data = re.sub(r'<','',row_data) # remove arrows + shot = row_data.split() #a list of shot data + if len(shot) == 5: #filter for main shots (5 fields - from, to, l, angl, incl) + three_shots.append(shot) # store first shot of main shots + #if the next shot name is different than the previous shot, save the single shot in three_shots, else append to the list + if len(three_shots) == 2 and (three_shots[1][0] != three_shots[0][0] or three_shots[1][1] != three_shots[0][1]): + main_shot = { + "t1": three_shots[0][0], + "t2": three_shots[0][1], + "l": f'{float(three_shots[0][2]):.3f}', + "a": f'{float(three_shots[0][3]):.3f}', + "f": f'{float(three_shots[0][4]):.3f}', + "left": "null", + "right": "null", + "up": "null", + "down": "null", + "note": "", + "flags": "" + } + self.cave_survey_json_data["viz"].append(main_shot) + three_shots.pop(0) # remove first shot from the list + elif len(three_shots) == 3: + mean_len = (float(three_shots[0][2])+float(three_shots[1][2])+float(three_shots[2][2]))/3 + mean_dir = (float(three_shots[0][3])+float(three_shots[1][3])+float(three_shots[2][3]))/3 + mean_inc = (float(three_shots[0][4])+float(three_shots[1][4])+float(three_shots[2][4]))/3 + main_shot = { + "t1": three_shots[0][0], + "t2": three_shots[0][1], + "l": mean_len, + "a": mean_dir, + "f": mean_inc, + "left": "null", + "right": "null", + "up": "null", + "down": "null", + "note": "", + "flags": "" + } + self.cave_survey_json_data["viz"].append(main_shot) + three_shots.clear() + with open(write_json_file_path, 'w') as file: + json.dump(self.cave_survey_json_data, file, indent=4) + parsed = True + return parsed + + def parse_topodroid(self): + with open(self.file_path,'r') as file: + date = next(file).split(',')[0][2:12] + self.survey_date = datetime.datetime.strptime(date, "%Y.%m.%d") + self.cave_name = next(file).split(',')[0][2:] + next(file) + next(file) + for row in file: + shot = row.split(',') + if shot[1] != '-': #filter for main shots + shot_from = shot[0][0:shot[0].find('@')] + shot_to = shot[1][0:shot[1].find('@')] + main_shot = { + "t1": shot_from, + "t2": shot_to, + "l": float(shot[2]), + "a": float(shot[3]), + "f": float(shot[4]), + "left": "null", + "right": "null", + "up": "null", + "down": "null", + "note": "", + "flags": "" + } + self.cave_survey_json_data["viz"].append(main_shot) + with open(write_json_file_path, 'w') as file: + json.dump(self.cave_survey_json_data, file, indent=4) + parsed = True + return parsed + + def parse_qave(self): + with open(self.file_path,'r') as file: + for _ in range(4): + next(file) + date = next(file).split(' ')[1].strip() + self.survey_date = datetime.datetime.strptime(date, "%Y-%m-%d") + next(file) + next(file) + for row in file: + shot = row.strip().split('\t') + if shot[1] != '-': #filter for main shots + main_shot = { + "t1": shot[0], + "t2": shot[1], + "l": float(shot[2]), + "f": float(shot[4]), + "a": float(shot[3]), + "left": "null", + "right": "null", + "up": "null", + "down": "null", + "note": "", + "flags": "" + } + self.cave_survey_json_data["viz"].append(main_shot) + else: + break + with open(write_json_file_path, 'w') as file: + json.dump(self.cave_survey_json_data, file, indent=4) + parsed = True + return parsed + + # MAGNETIC DECLINATION FUNCTIONS + def get_location(self, location): + global latitude, longitude + if location != '': + try: + self.lat_input.delete(0, ctk.END) + self.lon_input.delete(0, ctk.END) + lat_lon_app = Retrieve_lat_lon(location) + latitude, longitude = lat_lon_app.retrieve_lat_lon() + self.lat_input.insert(0, f'{latitude:.4f}') + self.lon_input.insert(0, f'{longitude:.4f}') + self.map.set_address(location, marker=True) + self.map.set_zoom(13) + except: + messagebox.showerror('Error','Nije moguće dohvatiti koordinate! Provjeri internet vezu!') + else: + messagebox.showerror('Error',f'{lcat["location_error"][self.lc]}') + + def get_md(self, model, year, month, day): + try: + datetime.datetime(int(year),int(month),int(day)) + except ValueError: + messagebox.showerror('Error',f'{lcat["datetime.ValueError"][self.lc]}') + if self.lat_input.get() == '' or self.lon_input.get() == '': + messagebox.showerror('Error',f'{lcat["lat_lon_input_error"][self.lc]}') + else: + try: + magn_decl_app = Retrieve_magn_decl(float(self.lat_input.get()), float(self.lon_input.get()), model, year, month, day) + self.md_val = float(f'{magn_decl_app.retrieve_magn_decl():.3f}') + self.md_value.configure(state='normal') + current_text = len(self.md_value.get()) + if current_text > 0: + self.md_value.delete(0, 'end') # override whatever present + self.md_value.insert(0,f'{self.md_val}°') # show md in magdec window + self.md_value.configure(state='disabled') # which you cannot change + current_text = len(self.show_md_value.get()) + if current_text > 0: + self.show_md_value.delete(0, 'end') # override whatever present + self.show_md_value.insert(0,f'{self.md_val}°') # refresh the md value in the main gui window + except: + messagebox.showerror('Error','Nije moguće izračunati magnetsku deklinaciju! Provjeri internet vezu!') + +if __name__ == '__main__': + lang, last_used = config() # Run config right at the start + SurveyScraper(lang, last_used) \ No newline at end of file