# LM: Only run pyuic5 on the original .ui (different file name).
#     pyuic5 generated code has been modified and extended to convert
#     log file format to GPX
#
#     Whatever licenses apply to inherited/generated/derivative code and ...
#     cc with attribution for my add-on part (Lloyd Milligan WA4EFS)
#
# From Qt Designer, referring to the .ui generated file:
#
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'log_to_GPX.ui'
# modified by LM to incorporate Log-to-GPX application code
#
# (GUI) Created by: PyQt5 UI code generator 5.15.11
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again.  Do not edit this file unless you know what you are doing.
#


import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QApplication, QFileDialog
import numpy as np
import json
import math
import locale
#from geopy.distance import great_circle
from geopy import distance

logFileName = ''
rxLog = [str]
xml = [str]
trkName = ''
logType = 0
data = list() # JSON

locale.setlocale(locale.LC_ALL, '') # For automatic number formatting in JSON analysis output
maxTempCorAlt = 13000 # Maximum altitude for which to compute altitude-temperature correlation

def	preConvert():  # Unreachable if rxLog is not complete
    global trkName
    if (logType == 3): # JSON (Sondehub uploads)
        trkName = str(data[0]['subtype']) + ' #' + str(data[0]['serial'])
    elif (logType == 2): # rdzTTGOsonde
        z = rxLog[2].split(',')
        trkName = z[2] + '-' + z[1]
    elif (logType == 1): # auto_rx
        trkName = rxLog[2].split(',')[1]
    return

def genHdr(hdrXml, name):
    global logType
    hdrXml.append('<?xml version="1.0" encoding="UTF-8"?>')
    hdrXml.append("<!-- GPX 1.0 Developer's Manual - https://www.topografix.com/gpx_manual.asp -->")
    hdrXml.append('<gpx xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.topografix.com/GPX/1/0" xsi:schemaLocation="http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd">')
    hdrXml.append("<trk>")
    hdrXml.append("<type>GPS Tracklog</type>; ")
    hdrXml.append("<name>Track: " + name + "</name>")
    if (logType == 3):
        hdrXml.append("<desc>From JSON data</desc>")
    elif (logType == 2):
        hdrXml.append("<desc>From rdzTTGOsonde Log</desc>")        
    elif (logType == 1):
        hdrXml.append("<desc>From Radiosonde auto_rx Log</desc>")
    else:
        hdrXml.append("<desc>Data source not identified</desc>")
    hdrXml.append("<extensions>")
    hdrXml.append("<!-- Suppress displaying track name over plotted track -->")
    hdrXml.append('<label xmlns="http://www.topografix.com/GPX/gpx_overlay/0/3">')
    hdrXml.append("</label>")
    hdrXml.append("</extensions>")

def genJsonTrkseg(trkXml, jsonDict):
    trkXml.append('<trkseg>')
    #Generate track points
    for j in range(0, len(jsonDict)):
        z = '<trkpt'
        z = z + ' lat="' + str(jsonDict[j]['lat']) + '"' + ' lon="' + str(jsonDict[j]['lon']) + '"' + '>'
        trkXml.append(z)
        trkXml.append('<time>' + jsonDict[j]['datetime'] + '</time>')
        trkXml.append('</trkpt>')
    trkXml.append('</trkseg>')

def genTrkseg(trkXml, log, strt):
    trkXml.append('<trkseg>')
    #Generate track points
    for i in range(strt, np.size(log)):
        y = log[i].split(',')
        if (len(y) > 4):
            z = '<trkpt'
            if (logType == 1): # auto_rx
                z = z + ' lat="'+y[3]+'"' + ' lon="'+y[4]+'"' + '>'
            elif (logType == 2): # rdzTTGOsonde
                z = z + ' lat="'+y[4]+'"' + ' lon="'+y[5]+'"' + '>'
            trkXml.append(z)
            trkXml.append('<time>' + y[0] + '</time>')
            trkXml.append('</trkpt>')
        else:
            break # Emergency break in case log has trailing (empty) lines
    trkXml.append('</trkseg>')

def genTrailer(endXml):
    endXml.append('</trk>')
    endXml.append('</gpx>')

class Ui_MainWindow(object):

    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(570, 540)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.label1 = QtWidgets.QLabel(self.centralwidget)
        self.label1.setGeometry(QtCore.QRect(40, 20, 49, 13))
        self.label1.setObjectName("label1")
        self.logFileNameEdit = QtWidgets.QLineEdit(self.centralwidget)
        self.logFileNameEdit.setGeometry(QtCore.QRect(90, 20, 401, 21))
        self.logFileNameEdit.setObjectName("logFileNameEdit")
        self.logFileNameEdit.textChanged.connect(self.logFileNameChanged)
        self.logFileNameEdit.returnPressed.connect(self.textLineReturn)
        self.browseButton = QtWidgets.QPushButton(self.centralwidget)
        self.browseButton.setGeometry(QtCore.QRect(500, 20, 31, 23))
        self.browseButton.setObjectName("browseButton")
        self.browseButton.clicked.connect(self.browseBtnClicked)
        self.convertButton = QtWidgets.QPushButton(self.centralwidget)
        self.convertButton.setGeometry(QtCore.QRect(450, 250, 80, 19))
        self.convertButton.setObjectName("convertButton")
        self.convertButton.clicked.connect(self.convertBtnClicked)
        self.exportButton = QtWidgets.QPushButton(self.centralwidget)
        self.exportButton.setGeometry(QtCore.QRect(450, 480, 80, 19))
        self.exportButton.setObjectName("exportButton")
        self.exportButton.clicked.connect(self.exportBtnClicked)
        self.loadButton = QtWidgets.QPushButton(self.centralwidget)
        self.loadButton.setGeometry(QtCore.QRect(30, 250, 75, 23))
        self.loadButton.setObjectName("loadButton")
        self.loadButton.clicked.connect(self.loadBtnClicked)
        self.logTextEdit = QtWidgets.QTextEdit(self.centralwidget)
        self.logTextEdit.setGeometry(QtCore.QRect(30, 50, 501, 191))
        self.logTextEdit.setObjectName("logTextEdit")
        self.gpxTextEdit = QtWidgets.QTextEdit(self.centralwidget)
        self.gpxTextEdit.setGeometry(QtCore.QRect(30, 280, 501, 191))
        self.gpxTextEdit.setObjectName("gpxTextEdit")
        self.jsonCheckBox = QtWidgets.QCheckBox(self.centralwidget)
        self.jsonCheckBox.setGeometry(QtCore.QRect(140, 480, 70, 17))
        self.jsonCheckBox.setObjectName("jsonCheckBox")
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 570, 21))
        self.menubar.setObjectName("menubar")
        self.menuFile = QtWidgets.QMenu(self.menubar)
        self.menuFile.setObjectName("menuFile")
        self.menuOptions = QtWidgets.QMenu(self.menubar)
        self.menuOptions.setObjectName("menuOptions")
        self.menuHelp = QtWidgets.QMenu(self.menubar)
        self.menuHelp.setObjectName("menuHelp")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)
        self.actionOpen = QtWidgets.QAction(MainWindow)
        self.actionOpen.setObjectName("actionOpen")
        self.actionJSON_Analysis = QtWidgets.QAction(MainWindow)
        self.actionJSON_Analysis.setObjectName("actionJSON_Analysis")
        self.actionHelp = QtWidgets.QAction(MainWindow)
        self.actionHelp.setObjectName("actionHelp")
        self.actionAbout = QtWidgets.QAction(MainWindow)
        self.actionAbout.setObjectName("actionAbout")
        self.actionQuit = QtWidgets.QAction(MainWindow)
        self.actionQuit.setObjectName("actionQuit")
        self.menuFile.addAction(self.actionOpen)
        self.menuFile.addAction(self.actionQuit)
        self.menuOptions.addAction(self.actionJSON_Analysis)
        self.menuHelp.addAction(self.actionHelp)
        self.menuHelp.addAction(self.actionAbout)
        self.menubar.addAction(self.menuFile.menuAction())
        self.menubar.addAction(self.menuOptions.menuAction())
        self.menubar.addAction(self.menuHelp.menuAction())
        # Add on for LilyGo log files
        self.rdzCheckBox = QtWidgets.QCheckBox(self.centralwidget)
        self.rdzCheckBox.setGeometry(QtCore.QRect(30, 480, 91, 17))
        self.rdzCheckBox.setObjectName("rdzCheckBox")
        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "Balloon Track to GPX"))
        self.label1.setText(_translate("MainWindow", "Log file:"))
        self.browseButton.setText(_translate("MainWindow", " ..."))
        self.convertButton.setText(_translate("MainWindow", "Convert"))
        self.exportButton.setText(_translate("MainWindow", "Export"))
        self.loadButton.setText(_translate("MainWindow", "Load"))
        self.rdzCheckBox.setText(_translate("MainWindow", "rdzTTGOsonde"))
        self.logTextEdit.clear() # Not needed
        self.gpxTextEdit.clear() # Ditto
        self.statusbar.showMessage('Default log type is auto_rx.   Check rdzTTGOsonde or JSON to change.')
        self.jsonCheckBox.setText(_translate("MainWindow", "JSON"))
        self.menuFile.setTitle(_translate("MainWindow", "File"))
        self.menuOptions.setTitle(_translate("MainWindow", "Options"))
        self.menuHelp.setTitle(_translate("MainWindow", "Help"))
        self.actionOpen.setText(_translate("MainWindow", "Open"))
        self.actionOpen.triggered.connect(self.menuActionOpen)
        self.actionJSON_Analysis.setText(_translate("MainWindow", "JSON Analysis"))
        self.actionJSON_Analysis.triggered.connect(self.menuActionJSON_Analysis)
        self.actionHelp.setText(_translate("MainWindow", "Help"))
        self.actionHelp.triggered.connect(self.menuActionHelp)
        self.actionAbout.setText(_translate("MainWindow", "About"))
        self.actionAbout.triggered.connect(self.menuActionAbout)
        self.actionQuit.setText(_translate("MainWindow", "Quit"))
        self.actionQuit.triggered.connect(self.menuActionQuit)
        # Tool tips
        self.logFileNameEdit.setToolTip(_translate("MainWindow", "<html><head/><body><p>Input file name</p></body></html>"))
        self.convertButton.setToolTip(_translate("MainWindow", "<html><head/><body><p>Convert to GPX</p></body></html>"))
        self.exportButton.setToolTip(_translate("MainWindow", "<html><head/><body><p>Output GPX file</p></body></html>"))
        self.loadButton.setToolTip(_translate("MainWindow", "<html><head/><body><p>Load Input File</p></body></html>"))
        self.browseButton.setToolTip(_translate("MainWindow", "<html><head/><body><p>Browse input file</p></body></html>"))



    def textLineReturn(self):
        self.statusbar.clearMessage()

    def refreshInputTextArea(self):
        self.logTextEdit.clear()
        self.logTextEdit.setStyleSheet("QTextEdit { background-color: rgb(255, 255, 255); }")

    def refreshOutputTextArea(self):
        self.gpxTextEdit.clear()
        self.gpxTextEdit.setStyleSheet("QTextEdit { background-color: rgb(255, 255, 255); }")

    def browseBtnClicked(self):
        self.statusbar.clearMessage()
        options = QFileDialog.Options()
        fname = QFileDialog.getOpenFileName(None, "Select file to load", "", "All Files (*);;Log Files (*.log);;Text Files (*.txt);;Data Files (*.json)")
        if (len(fname[0]) > 0):
            self.logFileNameEdit.setText(fname[0])
        else:
            self.statusbar.showMessage('No file selected!')
        

    def loadBtnClicked(self):
        global data, logFileName, logType, rxLog
        self.statusbar.clearMessage()
        if (len(logFileName) > 0):
            self.refreshInputTextArea()
            rxLog.clear()
            try:
                if (self.jsonCheckBox.isChecked()):
                    with open(logFileName.rstrip(), 'r', encoding='ascii') as json_file:
                        data = json.load(json_file)
                    json_file.close()
                    self.logTextEdit.append(str(data[0]))
                    self.logTextEdit.append('. . .')
                    self.logTextEdit.append('. . .')
                    self.logTextEdit.append('')
                    self.logTextEdit.append(str(data[len(data)-1]))
                else:
                    with open(logFileName.rstrip(), 'r', encoding='ascii') as file: # Strip newline from filename
                        for line in file:
                            rxLog.append(line) # Copy for processing
                            # To do: Reduce display copy for JSON type                        
                            self.logTextEdit.append(line) # Copy for display
                    self.refreshOutputTextArea() # Clear convert target on successful load
                    file.close()
                self.logTextEdit.verticalScrollBar().setValue(self.logTextEdit.verticalScrollBar().minimum())
                # Establish logType BEFORE converting (or analyzing) file/document
                if (self.jsonCheckBox.isChecked()): # JSON overrides rdz check box
                    self.statusbar.showMessage('Info: Document type is JSON.')
                    logType = 3                    
                elif (np.size(np.array(rxLog)) > 1):
                    if (self.rdzCheckBox.isChecked()):
                        self.statusbar.showMessage('Info: Document type is rdzTTGOsonde log.')
                        logType = 2
                    else:
                        self.statusbar.showMessage('Info: Document type is auto_rx log.')
                        logType = 1
            except:        
                        self.statusbar.showMessage('File open/read failed!')
        else:
            self.statusbar.showMessage('Please enter the name of a log file to load!')
    
    def convertBtnClicked(self):
        global logType, data, rxLog, trkName, xml
        self.statusbar.clearMessage()
        if (self.logTextEdit.document().isEmpty()):
            self.statusbar.showMessage('Please load file to be converted!')
            return # Insist on visible evidence of file in rxLog
        # self.statusbar.showMessage('Info: Converting document ...')
        # Conversion consumes CPU postponing above message display!
        try:
            preConvert()
            genHdr(xml, trkName)
            if (logType == 3):
                genJsonTrkseg(xml, data)
            else:
                genTrkseg(xml, rxLog, 2)
            genTrailer(xml)
            self.refreshOutputTextArea()
            for i in range(1, len(xml)):
                self.gpxTextEdit.append(xml[i])
            self.gpxTextEdit.verticalScrollBar().setValue(self.gpxTextEdit.verticalScrollBar().minimum())

        except:
            self.statusbar.showMessage('File conversion failed!   Possibly wrong file type...')
            
    def exportBtnClicked(self):
        global logFileName
        self.statusbar.clearMessage()
        if (len(xml) < 2):
            self.statusbar.showMessage('Please convert a log file. Nothing to export!')
            return
        if (len(logFileName) > 0):
            outFileName = logFileName.split('.')[0] + '.gpx'
            try:
                with open(outFileName, 'w') as fp:
                    for j in range(1, np.size(np.array(xml))):
                        fp.write("%s\n" % xml[j])
                    self.statusbar.showMessage('GPX file written!')
                fp.close()
            except:
                self.statusbar.showMessage('File open/write failed!')

    def logFileNameChanged(self):
        global logFileName
        logFileName = self.logFileNameEdit.text()
        self.refreshInputTextArea() # Clear both text areas on log file name change
        self.refreshOutputTextArea()
        self.statusbar.clearMessage() # And status bar

    def menuActionUndefined(self):
        self.statusbar.showMessage('The selected menu item is a placeholder.  Coding in progress.  Please try later!')

    def menuActionOpen(self):
        self.browseBtnClicked() # Same as Open (alias)

    def menuActionQuit(self):
        app.quit()
    
    def menuActionJSON_Analysis(self):
        global logType, data, trkName
        if (not (logType == 3)):
            self.statusbar.showMessage('Document type is not JSON. Please load a JSON file!')
            return
        if (len(data) == 0): # Should be impossible as other errors have higher priority
            self.statusbar.showMessage('No JSON data found. Please load a JSON file!')
            return
        self.gpxTextEdit.clear()
        self.gpxTextEdit.setStyleSheet("QTextEdit { background-color: rgb(255, 249, 230); }")
        self.gpxTextEdit.append('                                                                 JSON Data Summary')
        self.gpxTextEdit.append('')
        # Analyze first list element (test)
        d = data[0]
        trkName = str(d['subtype']) + ' #' + str(d['serial'])
        s = 'Balloon ' + trkName + ' was first detected'
        if ('alt' in d):
            alt = float(d['alt'])
            s = s + ' at ' + f'{alt:n}' +' meters'
        if ('uploader_callsign' in d):
            sta = str(d['uploader_callsign'])
            s = s + ' by station ' + sta + "."
        self.gpxTextEdit.append(s)
        if ('uploader_position' in d):
            if ('position' in d):
                sta_pos = d['uploader_position']
                bal_pos = d['position']
                s = 'Distance of balloon from reporting station ' + d['uploader_callsign']
                s = s + ' on first detection: '
                s = s + f'{distance.distance(sta_pos, bal_pos).km:n}'[:5] + ' kilometers'
                self.gpxTextEdit.append(s)
        self.gpxTextEdit.append('')
        # Initialize variables for main analysis loop
        sumX = sumY = sumXX = sumYY = sumXY = N = T = 0
        r = 0
        minAlt = 99999
        maxAlt = 0.
        d1 = {} # Scratch dictionaries
        d2 = {}
        d3 = {}
        minTemp = 999
        # Execute analysis loop
        for j in range(0, len(data)):
            d = data[j]
            # Insert dictionay validation code here
            if ('alt' in d):
                # Alt-only dependent analyses here
                z = float(d['alt'])
                if (z >= maxAlt):
                    # record last detected max altitude
                    maxAlt = z
                    d1 = d
                elif (z <= minAlt):
                    # record minimum altitude detected on descent (i.e. after maximum reached)
                    minAlt = z
                    d2 = d
                if ('temp' in d):
                    T = T + 1
                    # To do: Parameterize maximum altitude for correlation                    
                    if (z < maxTempCorAlt):
                        # Both alt and temp dependent analyses here
                        x = z # For Pearson r
                        y = d['temp']
                        if (float(y) < minTemp):
                            minTemp = float(y)
                            d3 = d
                        sumX = x + sumX
                        sumY = y + sumY
                        sumXX = x*x + sumXX
                        sumYY = y*y + sumYY
                        sumXY = x*y + sumXY
                        N = N + 1
        s = 'Of ' + f'{len(data):n}' + ' JSON data records, ' + f'{T:n}' + ' include both altitude and temperature.'
        self.gpxTextEdit.append(s)
        if (N > 30): # Minimum number of paired observations
            # Compute Pearson r
            num = N*sumXY-sumX*sumY
            den = math.sqrt((N*sumXX-sumX*sumX) * (N*sumYY-sumY*sumY))
            if (not (den == 0)):
                r = num/den
                # print ('r = ', r) # For tweaking maxTempCorAlt parameter value
                s = 'Pearson product-moment correlation of altitude and temperature below '
                s = s + f'{maxTempCorAlt:n}' + ' meters was ' + str(r)[:5] + '.'
                self.gpxTextEdit.append(s)
        self.gpxTextEdit.append('')
        if (maxAlt > 0):
            s = 'Maximum balloon altitude: ' + f'{maxAlt:n}' + ' meters'
            if ('uploader_callsign' in d1):
                s = s + ' (reported by station ' + d1['uploader_callsign'] + ')'
            self.gpxTextEdit.append(s)
            if ('uploader_position' in d1):
                if ('position' in d1):
                    sta_pos = d1['uploader_position']
                    bal_pos = d1['position']
                    s = 'Distance from reporting station ' + d1['uploader_callsign']
                    s = s + ' to balloon at maximum altitude: '
                    s = s + f'{distance.distance(sta_pos, bal_pos).km:n}'[:5] + ' kilometers'
                    self.gpxTextEdit.append(s)
            self.gpxTextEdit.append('')
        s = 'Lowest recorded temperature was ' + str(minTemp) + '° celsius'
        if ('alt' in d3):
            s = s + ' at ' + str(d3['alt']) + ' meters altitude'
        s = s + '.'
        self.gpxTextEdit.append(s)
        self.gpxTextEdit.append('')
        if (minAlt < 99999):
            s = 'Minimum altitude detected on descent was ' + f'{minAlt:n}' + ' meters,'
            if ('uploader_callsign' in d2):
                s = s + ' reported by station ' + d2['uploader_callsign'] + '.'
            self.gpxTextEdit.append(s)
            if ('uploader_position' in d2):
                if ('position' in d2):
                    sta_pos = d2['uploader_position']
                    bal_pos = d2['position']
                    s = 'Distance of balloon from reporting station ' + d2['uploader_callsign']
                    s = s + ' at last detected signal: '
                    s = s + f'{distance.distance(sta_pos, bal_pos).km:n}'[:5] + ' kilometers'
                    self.gpxTextEdit.append(s)
        self.gpxTextEdit.verticalScrollBar().setValue(self.gpxTextEdit.verticalScrollBar().minimum())
        self.statusbar.showMessage('JSON analysis coding in progress.')
        return

    def menuActionHelp(self):
        self.menuActionUndefined()
        return

    def menuActionAbout(self):
        self.menuActionUndefined()
        return

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()
    sys.exit(app.exec_())
