diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a1a85970635606d0513da94c62e1ca7dcd0bda91..ccc2977f043b6c3287d88055a1e4667a379855f0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -27,7 +27,7 @@ flake8: - passoft stage: Build Env and Test script: - - flake8 --exclude sohstationviewer/view/ui sohstationviewer + - flake8 --exclude sohstationviewer/view/ui,sohstationviewer/controller/core/ sohstationviewer - flake8 tests python3.7: diff --git a/sohstationviewer.py b/sohstationviewer.py old mode 100644 new mode 100755 index db43d6719cd02cf4b1216ab104172f4a5609b797..135ac8ed8533b8870838a64ced8f57ed69818d2a --- a/sohstationviewer.py +++ b/sohstationviewer.py @@ -1,10 +1,23 @@ +#!/usr/bin/env python3 +import platform +import os import sys - from PySide2 import QtWidgets from sohstationviewer.view.mainwindow import MainWindow +# Enable Layer-backing for MacOs version >= 11 +# Only needed if using the pyside2 library with version>=5.15. +# Layer-backing is always enabled in pyside6. +os_name, version, *_ = platform.platform().split('-') +# if os_name == 'macOS' and version >= '11': +# mac OSX 11.6 appear to be 10.16 when read with python and still required this +# environment variable +if os_name == 'macOS': + os.environ['QT_MAC_WANTS_LAYER'] = '1' + + def main(): app = QtWidgets.QApplication(sys.argv) wnd = MainWindow() diff --git a/sohstationviewer/conf/colorSettings.py b/sohstationviewer/conf/colorSettings.py new file mode 100755 index 0000000000000000000000000000000000000000..d38e6e7c316431fb6e9a1eada437613bcb8b48bf --- /dev/null +++ b/sohstationviewer/conf/colorSettings.py @@ -0,0 +1,92 @@ +# Just using RGB for everything since some things don't handle color names +# correctly, like PIL on macOS doesn't handle "green" very well. +# b = dark blue, was the U value for years, but it can be hard to see, so U +# was lightened up a bit. +# Orange should be #FF7F00, but #DD5F00 is easier to see on a white background +# and it still looks OK on a black background. +# Purple should be A020F0, but that was a little dark. +# "X" should not be used. Including X at the end of a passed color pair (or by +# itself) indicates that a Toplevel or dialog box should use grab_set_global() +# which is not a color. + +Clr = {"B": "#000000", "C": "#00FFFF", "G": "#00FF00", "M": "#FF00FF", + "R": "#FF0000", "O": "#FF7F00", "W": "#FFFFFF", "Y": "#FFFF00", + "E": "#DFDFDF", "A": "#8F8F8F", "K": "#3F3F3F", "U": "#0070FF", + "N": "#007F00", "S": "#7F0000", "y": "#7F7F00", "u": "#ADD8E6", + "s": "#FA8072", "p": "#FFB6C1", "g": "#90EE90", "r": "#EFEFEF", + "P": "#AA22FF", "b": "#0000FF"} + +# This is just if the program wants to let the user know what the possibilities +# are. +ClrDesc = {"B": "black", "C": "cyan", "G": "green", "M": "magenta", + "R": "red", "O": "orange", "W": "white", "Y": "yellow", + "E": "light gray", "A": "gray", "K": "dark gray", "U": "blue", + "N": "dark green", "S": "dark red", "y": "dark yellow", + "u": "light blue", "s": "salmon", "p": "light pink", + "g": "light green", "r": "very light gray", "P": "purple", + "b": "dark blue"} + + +def set_colors(mode): + """ + get the display_color dict according to mode + :param mode: "B" or "W" + """ + display_color = {} + if mode == "B": + # Main form background + display_color["MF"] = Clr["B"] + # GPS background and dots. + display_color["GS"] = Clr["K"] + display_color["GD"] = Clr["W"] + # Time rule time and rule. + display_color["TM"] = Clr["W"] + display_color["TR"] = Clr["Y"] + # Zoom markers + display_color["ZM"] = Clr["O"] + # Time grid lines. + display_color["GR"] = Clr["y"] + # Date/time and ticks. Different things want the colors expressed in + # different ways. + display_color["T0"] = Clr["W"] + display_color["TL"] = Clr["W"] + # Labels. + display_color["LB"] = Clr["C"] + # Text like the title. + display_color["TX"] = Clr["W"] + # The plot center line or the upper and lower bounds lines. + display_color["P0"] = Clr["A"] + # Line connecting dots. + display_color["PL"] = Clr["A"] + # Selector rules + display_color["SR"] = Clr["Y"] + # TPS Canvas + display_color["TC"] = Clr["B"] + # The main plot time rule created by others (like by TPS clicking). + display_color["OR"] = Clr["O"] + # Gap Gap and Overlap. + display_color["GG"] = Clr["R"] + display_color["GO"] = Clr["R"] + # RT130: + display_color["BAD/OFF"] = Clr["R"] + display_color["GOOD/ON"] = Clr["G"] + + elif mode == "W": + display_color["MF"] = Clr["W"] + display_color["GS"] = Clr["W"] + display_color["GD"] = Clr["B"] + display_color["TM"] = Clr["B"] + display_color["TR"] = Clr["U"] + display_color["GR"] = Clr["E"] + display_color["T0"] = Clr["B"] + display_color["TL"] = Clr["B"] + display_color["LB"] = Clr["B"] + display_color["TX"] = Clr["B"] + display_color["P0"] = Clr["A"] + display_color["PL"] = Clr["E"] + display_color["SR"] = Clr["A"] + display_color["TC"] = Clr["W"] + display_color["OR"] = Clr["U"] + display_color["GG"] = Clr["R"] + display_color["GO"] = Clr["R"] + return display_color diff --git a/sohstationviewer/conf/constants.py b/sohstationviewer/conf/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..d27768d785c5935791658d0f0a79c8b214b5ee4a --- /dev/null +++ b/sohstationviewer/conf/constants.py @@ -0,0 +1 @@ +HIGHEST_INT = 1E100 diff --git a/sohstationviewer/conf/dbSettings.py b/sohstationviewer/conf/dbSettings.py new file mode 100755 index 0000000000000000000000000000000000000000..de734017dc7217de7e4c51ade5ef3f3d52dad9d9 --- /dev/null +++ b/sohstationviewer/conf/dbSettings.py @@ -0,0 +1,16 @@ +import re + +""" +seisRE: Seismic data channels' regex: +First letter(Band Code): ABCDEFGHLMOPQRSTUV +Second letter (Instrument Code): GHLMN +Third letter (Orientation Code): ZNE123 +=> to check if the channel is seismic data: if conf['seisRE'].match(chan): +""" + +conf = { + 'dbpath': 'sohstationviewer/database/soh.db', + 'seisRE': re.compile('[A-HLM-V][GHLMN][ZNE123]'), + # +0.2:Y + 'valColRE': re.compile('^\+?\-?[0-9]+\.?[0-9]?:[RYGMC]') # noqa: W605 +} diff --git a/sohstationviewer/controller/core/__init__.py b/sohstationviewer/controller/core/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/sohstationviewer/controller/core/time_tmp.py b/sohstationviewer/controller/core/time_tmp.py index 74e9df879818ee984dd65266c0652cb780ec2e51..4f3c52f16b8aa53754cd7d0f46792c29c0543adf 100644 --- a/sohstationviewer/controller/core/time_tmp.py +++ b/sohstationviewer/controller/core/time_tmp.py @@ -3,9 +3,9 @@ Processing time functions that are derived exactly from qpeek/logpeek """ from time import gmtime, localtime, sleep, strftime, time from struct import pack, unpack -from sohstationviewer.controller.core.utils import intt, floatt, rtnPattern +# from sohstationviewer.controller.core.utils import intt, floatt, rtnPattern from obspy import UTCDateTime - +from sohstationviewer.controller.util import displayTrackingInfo # First day of the month for each non-leap year month MINUS 1. This will get # subtracted from the DOY, so a DOY of 91, minus the first day of April 90 # (91-90) will leave the 1st of April. The 365 is the 1st of Jan of the next @@ -44,8 +44,8 @@ def formatTime(parent, time, dateMode, timeMode=None): format = '%Y%m%d' elif dateMode == 'YYYY:DOY': format = '%Y:%j' - elif parent is not None: - parent.displayTrackingInfo("not defined format:'%s'" % dateMode, + else: + displayTrackingInfo(parent, "not defined format:'%s'" % dateMode, "error") if timeMode == 'HH:MM:SS': format += " %H:%M:%S" diff --git a/sohstationviewer/controller/plottingData.py b/sohstationviewer/controller/plottingData.py new file mode 100755 index 0000000000000000000000000000000000000000..545be6195fade756195bbfeb29ceaf04dea5485f --- /dev/null +++ b/sohstationviewer/controller/plottingData.py @@ -0,0 +1,214 @@ +""" +Functions that process data before plotting +""" + +import math + +from obspy import UTCDateTime + +from sohstationviewer.controller.util import displayTrackingInfo +from sohstationviewer.conf.dbSettings import conf + + +maxInt = 1E100 +maxFloat = 1.0E100 +# TODO: put this in DB +MassPosVoltRanges = {"regular": [0.5, 2.0, 4.0, 7.0], + "trillium": [0.5, 1.8, 2.4, 3.5]} +MassPosColorPallets = {"B": ["C", "G", "Y", "R", "M"], + "W": ["B", "B", "B", "B", "B"]} + + +def getMassposValueColors(rangeOpt, chan, cMode, errors, retType='str'): + if rangeOpt.lower() not in MassPosVoltRanges.keys(): + errors.append("%s: The current selected Mass Position color range " + "is '%s' isn't allowable. The accept ranges are: %s" + % (chan, rangeOpt, ', '.join(MassPosVoltRanges.keys()))) + print("ERRORS:", errors) + return + massPosVoltRange = MassPosVoltRanges[rangeOpt] + massPosColorPallet = MassPosColorPallets[cMode] + mul = -1 + valueColors = [] + """ + remove size for masspos because there are two many points for one plot + which make the line thicks + TODO: show something to let user know that it is clickable. + """ + for i in range(len(massPosVoltRange)): + if i % 2 == 0: + mul += 1 + if retType == 'str': + valueColors.append( + "%s:%s" % (massPosVoltRange[i], massPosColorPallet[i])) + else: + valueColors.append((massPosVoltRange[i], massPosColorPallet[i])) + if i == len(massPosVoltRange) - 1: + if retType == 'str': + valueColors.append( + "%s:+%s" % (massPosVoltRange[i], massPosColorPallet[i+1])) + else: + valueColors.append( + (massPosVoltRange[i], massPosColorPallet[i + 1])) + if retType == 'str': + return '|'.join(valueColors) + return valueColors + + +def formatTime(parent, time, dateMode, timeMode=None): + """ + :param parent: parent GUI to display tracking info + :param time: time to be format, can be UTCDateTime or epoch time + :param dateMode: the format of date + :param timeMode: the format of time + :return: the formated time string + """ + if isinstance(time, UTCDateTime): + t = time + else: + t = UTCDateTime(time) + + # https://docs.python.org/3/library/datetime.html# + # strftime-and-strptime-format-codes + format = '' + if dateMode == 'YYYY-MM-DD': + format = '%Y-%m-%d' + elif dateMode == 'YYYYMMDD': + format = '%Y%m%d' + elif dateMode == 'YYYY:DOY': + format = '%Y:%j' + else: + displayTrackingInfo(parent, + "not defined format:'%s'" % dateMode, + "error") + if timeMode == 'HH:MM:SS': + format += " %H:%M:%S" + + ret = t.strftime(format) + return ret + + +def getTitle(parent, setID, plottingData, dateMode): + """ + :param setID: (netID, statID, locID) + :param plottingData: a ditionary including: + { gaps: [(t1,t2),(t1,t2),...] (in epoch time) + channels:[cha: {netID, statID, locID, chanID, times, data, + startTmEpoch, endTmEpoch} + earliestUTC: the earliest time of all channels + latestUTC: the latest time of all channels + :return: title for the plot + """ + diff = plottingData['latestUTC'] - plottingData['earliestUTC'] + hours = diff/3600 + return ("Station: %s %s to %s (%s)" % + (setID[1], + formatTime(parent, plottingData['earliestUTC'], + dateMode, "HH:MM:SS"), + formatTime(parent, plottingData['latestUTC'], + dateMode, "HH:MM:SS"), + round(hours, 2)) + ) + + +def getGaps(gaps, gapMin): + """ + :param gaps: list of gaps + :param gapMin: minimum of gaps count in minutes + return list of gaps of which gaps smaller than gapMin have been removed + """ + gapMinSec = gapMin * 60 + return [g for g in gaps if (g[1] - g[0]) >= gapMinSec] + + +def getTimeTicks(earliest, latest, dateFormat, labelTotal): + """ + split time range into parts to use for tick labels + Ex: getTimeTicks(1595542860.0, 1595607663.91, 'YYYY-MM-DD', 3) + :param earliest: earliest epoch time + :param latest: latest epoch time + :param dateFormat: YYYY:DOY, YYYY-MM-DD or YYYYMMMDD + :param labelTotal: number of time label to be displayed, + others will show as ticks oly + :return: + times: list of times + majorTimes: list of time of lables to be displayed + majorTimelabels: list of labels displayed + """ + timeRange = latest - earliest + if timeRange >= 2592000.0: + mode = "DD" + # Time of the nearest midnight before the earliest time. + time = (earliest // 86400.0) * 86400 + interval = 864000.0 + elif timeRange >= 864000.0: + mode = "D" + time = (earliest // 86400.0) * 86400 + interval = 86400.0 + elif timeRange >= 3600.0: + mode = "H" + # Nearest hour. + time = (earliest // 3600.0) * 3600 + interval = 3600.0 + elif timeRange >= 60.0: + mode = "M" + # Nearest minute. + time = (earliest // 60.0) * 60 + interval = 60.0 + else: + mode = "S" + time = (earliest // 1) + interval = 1.0 + times = [] + timeLabels = [] + time += interval + while time < latest: + times.append(time) + timeLabel = formatTime(None, time, dateFormat, 'HH:MM:SS') + if mode == "DD" or mode == "D": + timeLabel = timeLabel[:-9] + elif mode == "H": + timeLabel = timeLabel[:-3] + elif mode == "M" or mode == "S": + timeLabel = timeLabel + timeLabels.append(timeLabel) + time += interval + + ln = len(timeLabels) + d = math.ceil(len(timeLabels) / labelTotal) + majorTimes = [times[i] for i in range(ln) if i % d == 0] + majorTimelabels = [timeLabels[i] for i in range(ln) if i % d == 0] + return times, majorTimes, majorTimelabels + + +def getUnitBitweight(chanDB, bitweightOpt): + """ + :param chanDB: channel's info got from database + :param bitweightOpt: qpeek's OPTBitWeightRVar + Commands: Show Seismic Data In Counts - None + Use Q330 Low Gain - Low + Use Q330 High Gain - High + :return: unit with fixed point decimal and bitweight if applicable + """ + plotType = chanDB['plotType'] + # Not all channels use/have this field. + unit = chanDB['unit'] + try: + fixPoint = chanDB['fixPoint'] + except Exception: + fixPoint = 0 + unitBitweight = '' + if plotType in ['linesDots', 'linesSRate', 'linesMasspos']: + if fixPoint == 0: + unitBitweight = "{}%s" % unit + else: + unitBitweight = "{:.%sf}%s" % (fixPoint, unit) + if conf['seisRE'].match(chanDB['channel']): + if fixPoint != 0: + unitBitweight = "{:.%sf}%s" % (fixPoint, unit) + else: + if bitweightOpt in ["low", "high"]: + unitBitweight = "{}V" + else: + unitBitweight = "{}%s" % unit + return unitBitweight diff --git a/sohstationviewer/controller/processing.py b/sohstationviewer/controller/processing.py old mode 100755 new mode 100644 index 14281dbd7807c78f3a24c491c124dd0208480756..87173891420575428f2adc25bb60c646522f27fc --- a/sohstationviewer/controller/processing.py +++ b/sohstationviewer/controller/processing.py @@ -1,17 +1,32 @@ -from sohstationviewer.model.mseed_text import MSeed_Text +import os +import json +import re +from obspy.core import read as read_ms +from obspy.io.reftek.core import Reftek130, Reftek130Exception +from sohstationviewer.model.mseed.mseed import MSeed +from sohstationviewer.model.reftek.reftek import RT130 +from sohstationviewer.database.extractData import signatureChannels +from sohstationviewer.controller.util import validateFile, displayTrackingInfo -def loadData(parent, listOfDir, reqInfoChans): + +def loadData(dataType, parent, listOfDir, reqInfoChans, reqDSs): """ Go through root dir and read all files in that dir and its subdirs """ dataObject = None for d in listOfDir: if dataObject is None: - # dataObject = Reftek.Reftek(parent, d) - # if dataObject.hasData(): - # continue - dataObject = MSeed_Text(parent, d, reqInfoChans) + if dataType == 'RT130': + dataObject = RT130(parent, d, reqInfoChans, reqDSs=reqDSs) + else: + try: + dataObject = MSeed(parent, d, reqInfoChans) + except Exception as e: + msg = f"Dir {d} can't be read due to error: {str(e)}" + displayTrackingInfo(parent, msg, "Warning") + pass + if dataObject.hasData(): continue # If no data can be read from the first dir, throw exception @@ -20,3 +35,89 @@ def loadData(parent, listOfDir, reqInfoChans): dataObject.readDir(d) return dataObject.plottingData + + +def readChannels(parent, listOfDir): + """ + Scan available channels for channel prefer dialog + """ + dataObject = None + for d in listOfDir: + if dataObject is None: + # dataObject = Reftek.Reftek(parent, d) + # if dataObject.hasData(): + # continue + dataObject = MSeed(parent, d, readChanOnly=True) + if len(dataObject.channels) == 0: + # If no data can be read from the first dir, throw exception + raise Exception("No data can be read from ", d) + else: + dataObject.readDir(d, readChanOnly=True) + return dataObject.channels + + +def detectDataType(parent, listOfDir): + """ + For each dir in listOfDir, use getDataTypeFromFile to identify the type + of data for that dir if find a signature channel + return: + + None if there are more than one types of data detected + + dataType found, Unknown data maybe return + """ + # Looks like an import was missing -- I'm guessing this wasn't + # running before the reftek stuff was put in?? + # sign_chan_dataType_dict = extractData.signatureChannels() + sign_chan_dataType_dict = signatureChannels() + dirDataTypeDict = {} + for d in listOfDir: + dataType = "Unknown" + for path, subdirs, files in os.walk(d): + for fileName in files: + if not validateFile(path, fileName): + continue + ret = getDataTypeFromFile(os.path.join(path, fileName), + sign_chan_dataType_dict) + if ret is not None: + dataType = ret + break + if dataType != "Unknown": + break + dirDataTypeDict[d] = dataType + dataTypeList = {d for d in dirDataTypeDict.values()} + if len(dataTypeList) > 1: + dirDataTypeStr = json.dumps(dirDataTypeDict) + dirDataTypeStr = re.sub(r'\{|\}|"', '', dirDataTypeStr) + dirDataTypeStr = re.sub(r', ', '\n', dirDataTypeStr) + msg = (f"There are more than one types of data detected:\n" + f"{dirDataTypeStr}\n\n" + f"Please have only data that related to each other.") + displayTrackingInfo(parent, msg, "error") + return + # elif list(dirDataTypeDict.values())[0] == "Can't be identified.": + # msg = (f"There are no known data detected.\n" + # f"Please select different folder(s).") + # displayTrackingInfo(parent, msg, "error") + # return + return list(dirDataTypeDict.values())[0] + + +def getDataTypeFromFile(filePath, sign_chan_dataType_dict): + stream = None + try: + stream = read_ms(os.path.join(filePath)) + except TypeError: + return + except Reftek130Exception: + pass + + if not stream: + try: + Reftek130.from_file(os.path.join(filePath)) + except (TypeError, Reftek130Exception): + return + return 'RT130' + + for trace in stream: + chan = trace.stats['channel'] + if chan in sign_chan_dataType_dict.keys(): + return sign_chan_dataType_dict[chan] diff --git a/sohstationviewer/controller/util.py b/sohstationviewer/controller/util.py new file mode 100644 index 0000000000000000000000000000000000000000..76e632bd63641f6e962c9ea31581e90cdc22f3fc --- /dev/null +++ b/sohstationviewer/controller/util.py @@ -0,0 +1,123 @@ +import os +import re +from datetime import datetime +from obspy import UTCDateTime + + +def validateFile(path, fileName): + if fileName.strip() == '.DS_Store' or fileName.startswith('._'): + # skip mac's info file + return False + + if not os.path.isfile(os.path.join(path, fileName)): + return False + return True + + +def displayTrackingInfo(parent, text, type='info'): + if parent is None: + print(f"{type}: {text}") + return + + msg = {'text': text} + if type == 'error': + msg['color'] = 'white' + msg['bgcolor'] = '#e46269' + elif type == 'warning': + msg['color'] = '#ffd966' + msg['bgcolor'] = 'orange' + else: + msg['color'] = 'blue' + msg['bgcolor'] = 'white' + htmlText = """<body> + <div style='color:%(color)s; background-color:%(bgcolor)s'> + %(text)s + </div> + </body>""" + parent.trackingInfoTextBrowser.setHtml(htmlText % msg) + parent.update() + + +def getTime6(timeStr): + """ + Get time from 6 parts string.Ex: 01:251:09:41:35:656/2001:251:09:41:35:656 + """ + year = timeStr.split(':')[0] + if len(year) == 2: + return getTime6_2y(timeStr) + else: + return getTime6_4y(timeStr) + + +def getTime6_2y(timeStr): + """ + Get time from 6 parts string.Ex: 01:251:09:41:35:656 + """ + # pad 0 so the last part has 6 digits to match with the format str + timeStr = timeStr.ljust(22, "0") + time = datetime.strptime(timeStr, "%y:%j:%H:%M:%S:%f") + utcTime = UTCDateTime(time) + return utcTime.timestamp, time.year + + +def getTime6_4y(timeStr): + """ + Get time from 6 parts string.Ex: 2001:251:09:41:35:656 + """ + # pad 0 so the last part has 6 digits to match with the format str + timeStr = timeStr.ljust(24, "0") + time = datetime.strptime(timeStr, "%Y:%j:%H:%M:%S:%f") + utcTime = UTCDateTime(time) + return utcTime.timestamp, time.year + + +def getTime4(timeStr, trackingYear, yAdded): + """ + Get time from 4 parts string. Ex: 253:19:41:42 + """ + if not yAdded: + # first day => move to next year + doy = timeStr.split(':') + if doy == 1: + trackingYear += 1 + yAdded = True + timeStr = f'{str(trackingYear)}:{timeStr}' + time = datetime.strptime(timeStr, "%Y:%j:%H:%M:%S") + utcTime = UTCDateTime(time) + return utcTime.timestamp, time.year, yAdded + + +def getVal(text): + """ + return value part including +/-, remove str that follows + """ + REVal = '^\+?\-?[0-9]+\.?[0-9]?' # noqa: W605 + return float(re.search(REVal, text).group()) + + +###################################### +# BEGIN: rtnPattern(In, Upper = False) +# LIB:rtnPattern():2006.114 - Logpeek +def rtnPattern(text, upper=False): + """ + return format of the string with: + + 0 for digit + + a for lowercase + + A for upper case + + remain special character + """ + rtn = "" + for c in text: + if c.isdigit(): + rtn += "0" + elif c.isupper(): + rtn += "A" + elif c.islower(): + rtn += "a" + else: + rtn += c + # So the A/a chars will always be A, so the caller knows what to look for + if upper is True: + return rtn.upper() + return rtn +# END: rtnPattern diff --git a/sohstationviewer/database/extractData.py b/sohstationviewer/database/extractData.py new file mode 100755 index 0000000000000000000000000000000000000000..215e563879c081ff221edf10769ffed942a3700a --- /dev/null +++ b/sohstationviewer/database/extractData.py @@ -0,0 +1,73 @@ +from sohstationviewer.database.proccessDB import executeDB_dict +from sohstationviewer.conf.dbSettings import conf + + +# key is last char of chan +SEIS_LABEL = {'1': 'NS', '2': 'EW', + 'N': 'NS', 'E': 'EW', 'Z': 'V'} + + +def getChanPlotInfo(orgChan, dataType): + """ + Given chanID read from raw data file and detected dataType + Return plotting info from DB for that channel + """ + chan = orgChan + if orgChan.startswith('EX'): + chan = 'EX?' + if orgChan.startswith('VM'): + chan = 'VM?' + if orgChan.startswith('MP'): + chan = 'MP?' + if orgChan.startswith('Event DS'): + chan = 'Event DS?' + if orgChan.startswith('DS'): + chan = 'DS?' + if orgChan.startswith('Disk Usage'): + chan = 'Disk Usage?' + if conf['seisRE'].match(chan): + chan = 'SEISMIC' + + sql = ("SELECT channel, plotType, height, unit, linkedChan," + " convertFactor, label, fixPoint, valueColors " + "FROM Channels as C, Parameters as P") + if dataType == 'Unknown': + sql = f"{sql} WHERE channel='{chan}' and C.param=P.param" + else: + sql = (f"{sql} WHERE channel='{chan}' and C.param=P.param" + f" and dataType='{dataType}'") + print("SQL:", sql) + chanInfo = executeDB_dict(sql) + + if len(chanInfo) == 0: + chanInfo = executeDB_dict( + f"{sql} WHERE channel='DEFAULT' and C.param=P.param") + else: + if chanInfo[0]['channel'] == 'SEISMIC': + chanInfo[0]['label'] = SEIS_LABEL[orgChan[-1]] + chanInfo[0]['channel'] = orgChan + + chanInfo[0]['label'] = ( + '' if chanInfo[0]['label'] is None else chanInfo[0]['label']) + chanInfo[0]['unit'] = ( + '' if chanInfo[0]['unit'] is None else chanInfo[0]['unit']) + chanInfo[0]['fixPoint'] = ( + 0 if chanInfo[0]['fixPoint'] is None else chanInfo[0]['fixPoint']) + if chanInfo[0]['label'].strip() == '': + chanInfo[0]['label'] = chanInfo[0]['channel'] + else: + chanInfo[0]['label'] = '-'.join([chanInfo[0]['channel'], + chanInfo[0]['label']]) + return chanInfo[0] + + +def signatureChannels(): + """ + return the dict {channel: dataType} in which channel is unique for dataType + """ + sql = ("SELECT channel, dataType FROM Channels where channel in" + "(SELECT channel FROM Channels GROUP BY channel" + " HAVING COUNT(channel)=1)") + rows = executeDB_dict(sql) + sign_chan_dataType_dict = {r['channel']: r['dataType'] for r in rows} + return sign_chan_dataType_dict diff --git a/sohstationviewer/database/proccessDB.py b/sohstationviewer/database/proccessDB.py new file mode 100755 index 0000000000000000000000000000000000000000..19edc92afe84072cda2abb62a10e98a7d1f7ba7e --- /dev/null +++ b/sohstationviewer/database/proccessDB.py @@ -0,0 +1,72 @@ +import sqlite3 + +from sohstationviewer.conf.dbSettings import conf + + +def executeDB(sql): + """ + used for both execute and query data + """ + conn = sqlite3.connect(conf['dbpath']) + cur = conn.cursor() + try: + cur.execute(sql) + except sqlite3.OperationalError as e: + print("sqlite3.OperationalError:%s\n\tSQL%s" % (str(e), sql)) + rows = cur.fetchall() + conn.commit() # used for execute: update/insert/delete + cur.close() + conn.close() + return rows + + +def executeDB_dict(sql): + """ + query data and return rows in dictionary with fields as keys + """ + conn = sqlite3.connect(conf['dbpath']) + conn.row_factory = sqlite3.Row + cur = conn.cursor() + try: + cur.execute(sql) + except sqlite3.OperationalError as e: + print("sqlite3.OperationalError:%s\n\tSQL%s" % (str(e), sql)) + rows = cur.fetchall() + cur.close() + conn.close() + return [dict(row) for row in rows] + + +def trunc_addDB(table, sqls): + """ + truncate table and refill with new data + """ + try: + conn = sqlite3.connect(conf['dbpath']) + cur = conn.cursor() + cur.execute('BEGIN') + cur.execute(f'DELETE FROM {table}') + for sql in sqls: + print("sql:", sql) + cur.execute(sql) + cur.execute('COMMIT') + except sqlite3.Error as e: + try: + cur.execute('ROLLBACK') + except Exception: + pass + return (f'Cannot write to table {table}' + f'because of database error:\n{str(e)}') + except Exception as e: + try: + cur.execute('ROLLBACK') + except Exception: + pass + return (f'Cannot write to table {table} ' + f'because of code error:\n{str(e)}') + try: + cur.close() + conn.close() + except Exception: + pass + return True diff --git a/sohstationviewer/database/resources/channels.csv b/sohstationviewer/database/resources/channels.csv new file mode 100755 index 0000000000000000000000000000000000000000..d06df94f3d5553564b8f9cb9befe7d93783e1654 --- /dev/null +++ b/sohstationviewer/database/resources/channels.csv @@ -0,0 +1,76 @@ +channel,label,param,convertFactor,unit,fixPoint,device +Internal Clock Phase Error,,Clock phase error,1,us,,RT130 +SEISMIC,,Seismic data,1,,,RT130 +VM?,,Mass position,1,v,1,RT130 +Battery voltage,,Input power supply voltage,1,v,,RT130 +Temperature,,Internal temperature,1,C,,RT130 +Disk usage disk 1/disk 2 (display both disks),,Buffer usage,1,GB,,RT130 +Latitude,,GNSS latitude,1,deg,,RT130 +Longitude,,GNSS longitude,1,deg,,RT130 +Elevation,,GNSS number of satellites used,1,m,,RT130 +DSP-clock difference,,Time uncertainty,1,ms,,RT130 +GPS On/Off/Error,,GPS On/Off/Error,1,,,RT130 +Jerks/DSP sets,,Jerks/DSP sets,1,,,RT130 +GPS Lock/Unlock,,GPS Lock/Unlock,1,,,RT130 +GPS Clock Power,,GPS Clock Power,1,,,RT130 +Dump called/Dump complete,,Dump call,1,,,RT130 +Acquisition started/Acquisition stopped,,Acquisition status,1,,,RT130 +Reset/power up,,Reset/power up,1,,,RT130 +Error/warnings,,Error/warnings,1,,,RT130 +Discrepancies,,Discrepancies,1,,,RT130 +SOH data definitions,,SOH data definitions,1,,,RT130 +Net Up/down,,Net Up/down,1,,,RT130 +Event DS1/DS2 etc,,Event DS1/DS2 etc,1,,,RT130 +Mass re-center,,Mass re-center,1,,,RT130 +LCE,PhaseError,Clock phase error,1,us,,Q330 +LCQ,ClockQual,Clock Quality,1,%,,Q330 +SEISMIC,SeismicData,Seismic data,1,,,Q330 +VM?,MassPos,Mass position,0.1,V,1,Q330 +VCO,VoltControllOscil,Oscillator value,1,,,Q330 +VEA,AntAmps,GPS antenna current,1,mA,,Q330 +VEC,SysAmps,Input power supply current,1,mA,,Q330 +VEP,InputVolts,Input power supply voltage,0.15,V,2,Q330 +VKI,SysTemp,Internal temperature,1,C,,Q330 +VPB,BufferUsed,Buffer usage,0.1,%,,Q330 +ACE,,Logging,1,,,Q330 +OCF,,Logging,1,,,Q330 +LOG,,Logging,1,,,Q330 +LCE,PhaseError,Clock phase error,1,us,,Centaur +LCQ,ClockQual,Clock Quality,1,%,,Centaur +SEISMIC,SeismicData,Seismic data,1,,,Centaur +VM?,MassPos,Mass position,0.001,V,1,Centaur +VCO,VoltControllOsci,Oscillator value,1,,,Centaur +VEC,AntAmps,Input power supply current,1,,,Centaur +VEI,SupplyVolts,Input power supply voltage,0.001,V,,Centaur +VDT,SysTemp,Internal temperature,0.001,C,,Centaur +VPB,BufferUsed,Buffer usage,1,%,,Centaur +EX?,ExtSOH,External SOH channels,1,uV,,Centaur +GAN,AntStatus,GNSS antenna status,1,,,Centaur +GEL,Elev,GNSS elevation,1,um,,Centaur +GLA,Lat,GNSS latitude,1,microdegrees,,Centaur +GLO,Lon,GNSS longitude,1,microdegrees,,Centaur +GNS,SatsUsed,GNSS number of satellites used,1,,,Centaur +GPL,PhaseLock,GNSS PPL status,1,,,Centaur +GST,GPS Off/Un/Lk,GNSS status,1,,,Centaur +LDO,BarometricPress,Barometric pressure,1,passcals,,Centaur +LIO,OutHumidity,Outdoor relative humidity,1,%,,Centaur +LKO,OutTemp,Outdoor temperature,1,mC,,Centaur +LWD,WindDir,Wind direction,1,degrees,,Centaur +LWS,WindSpeed,Wind speed,1,cm/s,,Centaur +VCE,AbsPhaseErr,Clock phase error,1,us,,Pegasus +VCQ,ClockQual,Clock Quality,1,%,,Pegasus +SEISMIC,SeismicData,Seismic data,1,,,Pegasus +VM?,MassPos,Mass position,0.000001,V,1,Pegasus +VCO,VoltControllOsci,Oscillator value,1,,,Pegasus +VE1,AntAmps,Input power supply current,0.001,mA,,Pegasus +VEI,SupplyVolts,Input power supply voltage,0.000001,V,,Pegasus +VDT,SysTemp,Internal temperature,0.000001,C,,Pegasus +VAN,AntStatus,GNSS antenna status,1,,1,Pegasus +VEL,Elev,GNSS elevation,1,,,Pegasus +VLA,Lat,GNSS latitude,1,,,Pegasus +VLO,Lon,GNSS longitude,1,,,Pegasus +VNS,SatsUsed,GNSS number of satellites used,1,,,Pegasus +VPL,PhaseLock,GNSS PPL status,1,,,Pegasus +VST,GPS Off/Un/Lk,GNSS status,1,,,Pegasus +VE2,SensorCurrent,Sensor current,1,,,Pegasus +ATU,TimeUncert,Time uncertainty,1,,,Pegasus \ No newline at end of file diff --git a/sohstationviewer/database/resources/dataTypes.csv b/sohstationviewer/database/resources/dataTypes.csv new file mode 100755 index 0000000000000000000000000000000000000000..bb329e5423457f87ed553aa2e775c238361d9565 --- /dev/null +++ b/sohstationviewer/database/resources/dataTypes.csv @@ -0,0 +1,5 @@ +dataType +RT130 +Q330 +Centaur +Pegasus \ No newline at end of file diff --git a/sohstationviewer/database/resources/database_note.txt b/sohstationviewer/database/resources/database_note.txt new file mode 100755 index 0000000000000000000000000000000000000000..c773ef96f516ee7193a74234785d294240cc69d1 --- /dev/null +++ b/sohstationviewer/database/resources/database_note.txt @@ -0,0 +1,61 @@ +SQLITE: https://www.tutorialspoint.com/sqlite/ +controller field$ sqlite3 soh.db +sqlite> .databases +main: /Users/field/Documents/GIT/sohstationviewer/sohstationviewer/controller/soh.db r/w +sqlite> create table test( + ...> col1 text, + ...> col2 int); +sqlite> .tables +sqlite> insert into test values ('lan', 43); +sqlite> select * from test; +sqlite> quit. + +PYTHON: https://www.datacamp.com/community/tutorials/sqlite-in-python +GUI for sqllite: + + download: https://nightlies.sqlitebrowser.org/osx/2020-01/ + + Start: open DB Browser for SQLite in Applications + + +Files - Import - Table from CSV file +Right-click on table choose modify table to add Primary key, Foreign key +Add foreign key: roll all the way to the right, double click on Foreign Key +column, the editing boxes will show up + + +import sqlite3 +conn = sqlite3.connect("soh.db") +cur = conn.cursor() +cur.execute('SELECT * from test') +# print("one row:", cur.fetchone()) +print("all rows:", cur.fetchall()) + +CREATE TABLE "DataTypes" ( + "dataType" TEXT NOT NULL, + PRIMARY KEY("dataType") +); +CREATE TABLE "Parameters" ( + "param" TEXT NOT NULL, + "plotType" TEXT, + "height" INTEGER, + PRIMARY KEY("param") +); +CREATE TABLE "Channels" ( + "channel" TEXT, + "param" TEXT, + "convertFactor" INTEGER, + "unit" TEXT, + "dataType" TEXT, + FOREIGN KEY("param") REFERENCES Parameters(param), + FOREIGN KEY("dataType") REFERENCES DataTypes(dataType), + PRIMARY KEY("channel","dataType") +); +CREATE TABLE "ChannelPrefer" ( + "name" TEXT NOT NULL, + "IDs" TEXT NOT NULL, + "dataType" TEXT, + "current" INTEGER, + PRIMARY KEY("name") +); + +INSERT INTO Channels +SELECT * FROM chan where dataType="RT130"; \ No newline at end of file diff --git a/sohstationviewer/database/resources/parameters.csv b/sohstationviewer/database/resources/parameters.csv new file mode 100755 index 0000000000000000000000000000000000000000..f53a904c3c6b0a4267afd83310d66bf620489c86 --- /dev/null +++ b/sohstationviewer/database/resources/parameters.csv @@ -0,0 +1,40 @@ +param,plotType,height +Clock phase error,lines,3 +Clock Quality,lines,3 +Seismic data,linesSRate,4 +Mass position,linesMasspos,4 +Oscillator value,lines,2 +GPS antenna current,lines,2 +Input power supply current,lines,2 +Input power supply voltage,lines,2 +Internal temperature,lines,2 +Buffer usage,lines,2 +External SOH channels,,0 +GNSS antenna status,dotsRM,2 +GNSS elevation,lines,2 +GNSS latitude,lines,2 +GNSS longitude,lines,2 +GNSS number of satellites used,lines,2 +GNSS PPL status,dotsRYGM,2 +GNSS status,dotsRM,2 +Barometric pressure,,0 +Outdoor relative humdity,,0 +Outdoor temperature,,0 +Wind direction,,0 +Wind speed,,0 +Sensor current,lines,2 +Time uncertainty,lines,2 +GPS On/Off/Error,dotZeroOneRG,2 +Jerks/DSP sets,dotZeroOneRY,2 +GPS Lock/Unlock,lines,2 +GPS Clock Power,dotsGCYRM,2 +Dump call,dotZeroOneRG,2 +Acquisition status,dotZeroOneRG,2 +Reset/power up,dotZeroOneWC,2 +Error/warnings,dotZeroOneRY,2 +Discrepancies,dotsRYGM,2 +SOH data definitions,dotZeroOneWC,2 +Net Up/down,dotZeroOneRG,2 +Event DS1/DS2 etc,linesDots,2 +Mass re-center,dotForTime,2 +Logging,,0 \ No newline at end of file diff --git a/sohstationviewer/database/soh.db b/sohstationviewer/database/soh.db new file mode 100755 index 0000000000000000000000000000000000000000..5896e88fc9b570cf28bdca5cc1510bc76cde45f8 Binary files /dev/null and b/sohstationviewer/database/soh.db differ diff --git a/sohstationviewer/model/core/dataType.py b/sohstationviewer/model/core/dataType.py deleted file mode 100755 index a7be3da3fef32e50679049ecbf9f6039db05fa00..0000000000000000000000000000000000000000 --- a/sohstationviewer/model/core/dataType.py +++ /dev/null @@ -1,64 +0,0 @@ -import os - - -class WrongDataTypeError(Exception): - def __init__(self, *args, **kwargs): - self.args = (args, kwargs) - - -class DataType: - def __init__(self, parent, dir, reqInfoChans=[]): - self.parent = parent - self.dir = dir - self.reqInfoChans = reqInfoChans - self.noneReqChans = set() - self.curNetStatLoc = ('_', '_', '_') - self.processingLog = [] # [(message, type)] - self.logData = {} - self.plottingData = {} - self.streams = {} - - self.readDir(dir) - - def readFile(self, pathToFile): - pass - - def combineData(self): - """ - Merge channels in each stream - """ - pass - - def readDir(self, dir): - count = 0 - for path, subdirs, files in os.walk(dir): - - for fileName in files: - if fileName.startswith('._'): - # skip mac's info file - continue - if not os.path.isfile(os.path.join(path, fileName)): - continue - self.readFile(path, fileName) - count += 1 - if count % 50 == 0: - self.displayTrackingInfo("Reading file %s" % count, - 'info') - self.combineData() - - def hasData(self): - if len(self.logData) == 0 and len(self.plottingData) == 0: - return False - return True - - def displayTrackingInfo(self, text, type): - """ - :param text: text to be displayed - :param type: info/warning/error - """ - print("displayTrackingInfo:", text) - self.parent.displayTrackingInfo(text, type) - - def trackInfo(self, text, type): - self.displayTrackingInfo(text, type) - self.processingLog.append((text, type)) diff --git a/sohstationviewer/model/dataType.py b/sohstationviewer/model/dataType.py new file mode 100644 index 0000000000000000000000000000000000000000..c5c37ecc626a6b72ab296ed405e9e8827acc5d5e --- /dev/null +++ b/sohstationviewer/model/dataType.py @@ -0,0 +1,102 @@ +import os + +from sohstationviewer.controller.util import validateFile, displayTrackingInfo +from sohstationviewer.conf.dbSettings import conf + + +class WrongDataTypeError(Exception): + def __init__(self, *args, **kwargs): + self.args = (args, kwargs) + + +class DataType(): + def __init__(self, parent, dir, reqInfoChans=[], + readChanOnly=False, reqDSs=[]): + print("reqDSs:", reqDSs) + self.parentGUI = parent + self.dir = dir + self.reqInfoChans = reqInfoChans + self.noneReqChans = set() + self.curKey = ('_', '_', '_') + self.processingLog = [] # [(message, type)] + self.logData = {} + self.plottingData = {} + self.streams = {} + self.channels = set() + self.reqDSs = reqDSs + self.readDir(dir, readChanOnly) + + def checkChan(self, chanID): + if self.reqInfoChans == []: + return True + if chanID in self.reqInfoChans: + return True + if 'EX?' in self.reqInfoChans and chanID.startswith('EX'): + if chanID[2] in ['1', '2', '3']: + return True + if 'VM?' in self.reqInfoChans and chanID.startswith('VM'): + if chanID[2] in ['0', '1', '2', '3', '4', '5', '6']: + return True + if 'SEISMIC' in self.reqInfoChans and conf['seisRE'].match(chanID): + return True + return False + + def readFile(self, pathToFile): + pass + + def combineData(self): + """ + Merge channels in each stream + """ + pass + + def readDir(self, dir, readChanOnly=False): + self.readChanOnly = readChanOnly + count = 0 + for path, subdirs, files in os.walk(dir): + for fileName in files: + if not validateFile(path, fileName): + continue + self.readFile(path, fileName) + count += 1 + if count % 50 == 0: + displayTrackingInfo( + self.parentGUI, "Reading file %s" % count) + if readChanOnly: + return + self.combineData() + # print("logs:", self.logData[('A195', 0, 19)].keys()) + # print("logs:", self.logData[('A195', 0, 19)]['SH']) + + def hasData(self): + if len(self.logData) == 0 and len(self.plottingData) == 0: + return False + return True + + def trackInfo(self, text, type): + displayTrackingInfo(self.parent, text, type) + if type != 'info': + self.processingLog.append((text, type)) + + def readText(self, path, fileName): + """ + Read log file and add to logData under channel TEXT + """ + if self.readChanOnly and 'LOG' in self.channels: + return + with open(os.path.join(path, fileName), 'r') as file: + try: + content = file.read() + except UnicodeDecodeError: + self.trackInfo("Can't process file: %s" % fileName, 'error') + return + if self.readChanOnly: + self.channels.add('LOG') + logText = "\n\n** STATE OF HEALTH: %s\n" % fileName + logText += content + self.addLog('TEXT', logText) + + def addLog(self, chan_pkt, logInfo): + if chan_pkt not in self.logData[self.curKey].keys(): + self.logData[self.curKey][chan_pkt] = [] + self.logData[self.curKey][chan_pkt].append(logInfo) diff --git a/sohstationviewer/model/mseed/__init__.py b/sohstationviewer/model/mseed/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/sohstationviewer/model/mseed/blockettes_reader.py b/sohstationviewer/model/mseed/blockettes_reader.py new file mode 100644 index 0000000000000000000000000000000000000000..a627fd3d4cccaad00c907a7982262313997d6385 --- /dev/null +++ b/sohstationviewer/model/mseed/blockettes_reader.py @@ -0,0 +1,127 @@ +import os +from struct import unpack + + +class ReadBlocketteError(Exception): + def __init__(self, errno, msg): + self.msg = msg + + +def readASCII(path, fileName, byteorder): + """ + test function + """ + file = open(os.path.join(path, fileName), 'rb') + databytes = file.read() + file.close() + followingBlktsTotal = unpack('%s%s' % (byteorder, 'B'), + databytes[39:40])[0] + if followingBlktsTotal > 1: + nextBlktByteNo = 48 + 8 # header + blkt1000(SEED info) + else: + nextBlktByteNo = 0 + logText = "" + while nextBlktByteNo != 0: + nextBlktByteNo, info = readNextBlkt( + nextBlktByteNo, databytes, byteorder) + logText += info + + print("INFO:\n", logText) + + +# ________________ based on mseedpeek.libtrace.Mseed.blkXXX________________ +# https://docs.python.org/3/library/struct.html +# SEED Manual V2.4 - Chapter 8 - Data Record +def readNextBlkt(bNo, databytes, byteorder): + """ + :param bNo: next blockette Byte Number + :param databytes: file's data in byte + :param byteorder: big/little endian + :param key: (net, stat, loc) + :param chanID: channel + :return: + nextBNo: byte number of next blockette + info: info read from this blockette + """ + blocketteType = unpack('%s%s' % (byteorder, 'H'), + databytes[bNo:bNo + 2])[0] + nextBNo = unpack('%s%s' % (byteorder, 'H'), + databytes[bNo + 2:bNo + 4])[0] + + try: + # readBlkt will skip first 4 bytes (HH) as they are already read + info = eval("readBlkt%s(%s, %s, '%s')" % + (blocketteType, bNo, databytes, byteorder)) + except NameError: + raise ReadBlocketteError( + f"Function to read blockette {blocketteType} isn't implemented " + f"yet. Implementer needs to write and add the function to the " + f"dict self.readBlkt in Mseed.__init__()") + return nextBNo, info + + +def readBlkt500(bNo, databytes, byteorder): + logText = "\nVCO Correction: %s" % unpack( + '%s%s' % (byteorder, 'f'), databytes[bNo + 4:bNo + 8])[0] + t = {} + (t['year'], t['doy'], t['hour'], t['min'], t['sec'], + junk, t['micro']) = unpack( + '%s%s' % (byteorder, 'HHBBBBH'), databytes[bNo + 8:bNo + 18]) + logText += ("\nTime of exception: %(year)s:%(doy)s:%(hour)s:%(min)s:" + "%(sec)s:%(micro)s" % t) + logText += "\nMicro sec: %s" % unpack( + '%s%s' % (byteorder, 'B'), databytes[bNo + 18:bNo + 19])[0] + logText += "\nReception Quality: %s" % unpack( + '%s%s' % (byteorder, 'B'), databytes[bNo + 19:bNo + 20])[0] + logText += "\nException Count: %s" % unpack( + '%s%s' % (byteorder, 'I'), databytes[bNo + 20:bNo + 24])[0] + logText += "\nException Type: %s" % unpack( + '%s%s' % (byteorder, '16s'), databytes[bNo + 24:bNo + 40])[0].strip() + logText += "\nClock Model: %s" % unpack( + '%s%s' % (byteorder, '32s'), databytes[bNo + 40:bNo + 72])[0].strip() + logText += "\nClock Status: %s" % unpack( + '%s%s' % (byteorder, '128s'), databytes[bNo + 72:bNo + 200])[0].strip() + return logText + + +def readBlkt2000(bNo, databytes, byteorder): + blktLen = unpack( + '%s%s' % (byteorder, 'H'), databytes[bNo + 4:bNo + 6])[0] + logText = "\nTotal Blockette length: %s bytes" % blktLen + logText += "\nOffset to Opaque Data: %s" % unpack( + '%s%s' % (byteorder, 'H'), databytes[bNo + 6:bNo + 8])[0] + logText += "\nRecord number: %s" % unpack( + '%s%s' % (byteorder, 'I'), databytes[bNo + 8:bNo + 12])[0] + logText += "\nData Word order: %s" % unpack( + '%s%s' % (byteorder, 'B'), databytes[bNo + 12:bNo + 13])[0] + logText += "\nOpaque Data flags: %s" % unpack( + '%s%s' % (byteorder, 'B'), databytes[bNo + 13:bNo + 14])[0] + opaqueHeaderTotal = unpack( + '%s%s' % (byteorder, 'B'), databytes[bNo + 14:bNo + 15])[0] + logText += "\nNumber of Opaque Header fields: %s" % opaqueHeaderTotal + n = bNo + 15 + c = 0 + headerLen = 0 + for i in range(opaqueHeaderTotal): + hfield = '' + hchar = '' + while hchar != '~': + hfield += hchar + hchar = unpack( + '%s%s' % (byteorder, '1s'), databytes[n + c:n + c + 1])[ + 0].decode() + headerLen = c + 1 + c += 1 + logText += "\nOpaque Header %s: %s" % (i, hfield) + + opaqueDataLength = blktLen - 15 - headerLen + logText += "\nOpaque Data: %s" % unpack( + '%s%s' % (byteorder, '%ss' % opaqueDataLength), + databytes[n + headerLen: n + headerLen + opaqueDataLength]) + + return logText + + +if __name__ == '__main__': + readASCII( + "/Volumes/UNTITLED/fromCloud/qpeek/5244.sdr", "DT0001__.OCF", ">") diff --git a/sohstationviewer/model/mseed_text.py b/sohstationviewer/model/mseed/mseed.py old mode 100755 new mode 100644 similarity index 71% rename from sohstationviewer/model/mseed_text.py rename to sohstationviewer/model/mseed/mseed.py index 7218004d9ef1bccbc8cca68faaf3e5d8d456b248..41fc063e3d25bfb4aa8196a2606a1405a9c8559e --- a/sohstationviewer/model/mseed_text.py +++ b/sohstationviewer/model/mseed/mseed.py @@ -4,13 +4,16 @@ from obspy.core import Stream, read as read_ms from obspy import UTCDateTime from struct import unpack -from sohstationviewer.model.core.dataType import DataType +from sohstationviewer.model.dataType import DataType +from sohstationviewer.model.mseed.blockettes_reader import ( + readNextBlkt, ReadBlocketteError) +from sohstationviewer.conf import constants -class MSeed_Text(DataType): - def __init__(self, *kwarg): - self.readBlkt = {500: self.readBlkt500} - super().__init__(*kwarg) + +class MSeed(DataType): + def __init__(self, parent, dir, reqInfoChans=[], readChanOnly=False): + super().__init__(parent, dir, reqInfoChans, readChanOnly) def readFile(self, path, fileName): if not self.readMiniseed(path, fileName): @@ -36,8 +39,8 @@ class MSeed_Text(DataType): minStarttime = startTm if endTm > maxEndtime: maxEndtime = endTm - self.plottingData[k]['earliestUTC'] = minStarttime - self.plottingData[k]['latestUTC'] = maxEndtime + self.plottingData[k]['earliestUTC'] = minStarttime.timestamp + self.plottingData[k]['latestUTC'] = maxEndtime.timestamp def addLog(self, chan, logText): if chan not in self.logData[self.curNetStatLoc].keys(): @@ -48,12 +51,16 @@ class MSeed_Text(DataType): """ Read log file and add to logData under channel TEXT """ + if self.readChanOnly and 'LOG' in self.channels: + return with open(os.path.join(path, fileName), 'r') as file: try: content = file.read() except UnicodeDecodeError: self.trackInfo("Can't process file: %s" % fileName, 'error') return + if self.readChanOnly: + self.channels.add('LOG') logText = "\n\n** STATE OF HEALTH: %s\n" % fileName logText += content self.addLog('TEXT', logText) @@ -67,11 +74,13 @@ class MSeed_Text(DataType): stream = read_ms(os.path.join(path, fileName)) except TypeError: return False - file = None for trace in stream: chanID = trace.stats['channel'] - if chanID not in self.reqInfoChans: + if self.readChanOnly: + self.channels.add(chanID) + continue + if not self.checkChan(chanID): self.noneReqChans.add(chanID) continue netID = trace.stats['network'] @@ -79,7 +88,7 @@ class MSeed_Text(DataType): locID = trace.stats['location'] self.curNetStatLoc = k = (netID, statID, locID) if trace.stats.mseed['encoding'] == 'ASCII': - file = self.readASCII(path, fileName, file, trace, k) + file = self.readASCII(path, fileName, file, trace, k, chanID) else: if k not in self.streams.keys(): self.streams[k] = Stream() @@ -88,58 +97,6 @@ class MSeed_Text(DataType): file.close() return True - def readASCII(self, path, fileName, file, trace, k): - byteorder = trace.stats.mseed['byteorder'] - h = trace.stats - logText = "\n\n**** STATE OF HEALTH: " - logText += ("From:%s To:%s\n" % (h.starttime, h.endtime)) - textFromData = trace.data.tobytes().decode() - if textFromData != '': - logText += textFromData - else: - recLen = h.mseed['record_length'] - if file is None: - file = open(os.path.join(path, fileName), 'rb') - databytes = file.read(recLen) - followingBlktNum = unpack('%s%s' % (byteorder, 'B'), - databytes[39:40])[0] - if followingBlktNum > 1: - # skip blockette 1000 - nextBlocketteType = unpack('%s%s' % (byteorder, 'H'), - databytes[56:58])[0] - try: - logText += self.readBlkt[nextBlocketteType](databytes, - byteorder) - except KeyError: - msg = ("Function to read blockette %s isn't implemented " - "yet. Implementer needs to write and add the " - "function to the dict self.readBlkt in Mseed_Text." - "__init__()" % nextBlocketteType) - self.trackInfo(msg, 'error') - return file - if k not in self.logData: - self.logData[k] = {} - if h.channel not in self.logData[k]: - self.logData[k][h.channel] = [] - self.logData[k][h.channel].append(logText) - return file - - def readBlkt500(self, databytes, byteorder): - t = {} - (vcocorr, t['year'], t['doy'], t['hour'], t['min'], t['sec'], - junk, t['micro'], t['micro2'], qual, cnt, type, mod, stat - ) = unpack('%s%s' % (byteorder, 'fHHBBBBHBBI16s32s128s'), - databytes[60:256]) - logText = "\nVCO Correction: %s" % vcocorr - logText += ("\nTime of exception: %(year)s:%(doy)s:%(hour)s:%(min)s:" - "%(sec)s:%(micro)s:%(micro2)s" % t) - logText += "\nReception Quality: %s" % qual - logText += "\nException Count: %s" % cnt - logText += "\nException Type: %s" % type.strip() - logText += "\nClock Model: %s" % mod.strip() - logText += "\nClock Status: %s" % stat.strip() - return logText - def readTrace(self, trace, channels): chan = {} chan['netID'] = trace.stats['network'] @@ -188,12 +145,12 @@ class MSeed_Text(DataType): msg = "Number of Gaps for different channel are not equal.\n" for k in gaps_dict.keys(): msg += "%s: %s\n" % (k, len(gaps_dict[k])) - self.displayTrackingInfo(msg, 'error') + self.trackInfo(msg, 'error') return [] squashedGaps = [] for i in range(firstLen): maxEndtime = 0 - minStarttime = 1E100 + minStarttime = constants.HIGHEST_INT for val in gaps_dict.values(): if val[i][0] < minStarttime: minStarttime = val[i][0] @@ -201,3 +158,37 @@ class MSeed_Text(DataType): maxEndtime = val[i][1] squashedGaps.append((minStarttime, maxEndtime)) return squashedGaps + + def readASCII(self, path, fileName, file, trace, key, chanID): + byteorder = trace.stats.mseed['byteorder'] + h = trace.stats + logText = "\n\n**** STATE OF HEALTH: " + logText += ("From:%s To:%s\n" % (h.starttime, h.endtime)) + textFromData = trace.data.tobytes().decode() + if textFromData != '': + logText += textFromData + else: + recLen = h.mseed['record_length'] + if file is None: + file = open(os.path.join(path, fileName), 'rb') + databytes = file.read(recLen) + followingBlktsTotal = unpack('%s%s' % (byteorder, 'B'), + databytes[39:40])[0] + if followingBlktsTotal > 1: + nextBlktByteNo = 48 + 8 # header + blkt1000(SEED info) + else: + nextBlktByteNo = 0 + while nextBlktByteNo != 0: + try: + nextBlktByteNo, info = readNextBlkt( + nextBlktByteNo, databytes, byteorder) + logText += info + except ReadBlocketteError as e: + self.trackInfo(f"{key} - {chanID}: {e.msg}", 'error') + + if key not in self.logData: + self.logData[key] = {} + if h.channel not in self.logData[key]: + self.logData[key][h.channel] = [] + self.logData[key][h.channel].append(logText) + return file diff --git a/sohstationviewer/model/reftek/__init__.py b/sohstationviewer/model/reftek/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/sohstationviewer/model/reftek/from_rt2ms/__init__.py b/sohstationviewer/model/reftek/from_rt2ms/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d3158d957c5862b567d641bd24c72c6a1397464a --- /dev/null +++ b/sohstationviewer/model/reftek/from_rt2ms/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- + +"""Top-level package for from_rt2ms.""" + +__author__ = """IRIS PASSCAL""" +__email__ = 'software-support@passcal.nmt.edu' +__version__ = '2021.238' diff --git a/sohstationviewer/model/reftek/from_rt2ms/core.py b/sohstationviewer/model/reftek/from_rt2ms/core.py new file mode 100644 index 0000000000000000000000000000000000000000..68f2931f2898761ddcfbc324f08ea2cf75c565a8 --- /dev/null +++ b/sohstationviewer/model/reftek/from_rt2ms/core.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Suggested updates to obspy.io.reftek.core: +- modify section of the code that deals with upacking data + with '16' and '32' encodings to account for number of samples in each data + packet. Mass position channels data are '16' encoded. + +Maeva Pourpoint IRIS/PASSCAL +""" + + +import copy +import obspy.io.reftek.core as obspy_rt130_core +import warnings + +import numpy as np + +from obspy import Trace, Stream, UTCDateTime +from obspy.core.util.obspy_types import ObsPyException +from obspy.io.reftek.packet import _unpack_C0_C2_data +from sohstationviewer.model.reftek.from_rt2ms.packet import EHPacket + + +class Reftek130Exception(ObsPyException): + pass + + +class Reftek130(obspy_rt130_core.Reftek130): + + def to_stream(self, network="", location="", component_codes=None, + headonly=False, verbose=False, + sort_permuted_package_sequence=False): + """ + :type headonly: bool + :param headonly: Determines whether or not to unpack the data or just + read the headers. + """ + if verbose: + print(self) + if not len(self._data): + msg = "No packet data in Reftek130 object (file: {})" + raise Reftek130Exception(msg.format(self._filename)) + self.check_packet_sequence_and_sort(sort_permuted_package_sequence) + self.check_packet_sequence_contiguous() + self.drop_not_implemented_packet_types() + if not len(self._data): + msg = ("No packet data left in Reftek130 object after dropping " + "non-implemented packets (file: {})").format(self._filename) + raise Reftek130Exception(msg) + st = Stream() + for event_number in np.unique(self._data['event_number']): + data = self._data[self._data['event_number'] == event_number] + # we should have exactly one EH and one ET packet, truncated data + # sometimes misses the header or trailer packet. + eh_packets = data[data['packet_type'] == b"EH"] + et_packets = data[data['packet_type'] == b"ET"] + if len(eh_packets) == 0 and len(et_packets) == 0: + msg = ("Reftek data (file: {}) contain data packets without " + "corresponding header or trailer packet." + .format(self._filename)) + raise Reftek130Exception(msg) + if len(eh_packets) > 1 or len(et_packets) > 1: + msg = ("Reftek data (file: {}) contain data packets with " + "multiple corresponding header or trailer packets." + .format(self._filename)) + raise Reftek130Exception(msg) + if len(eh_packets) != 1: + msg = ("No event header (EH) packets in packet sequence. " + "File ({}) might be truncated.".format(self._filename)) + warnings.warn(msg) + if len(et_packets) != 1: + msg = ("No event trailer (ET) packets in packet sequence. " + "File ({}) might be truncated.".format(self._filename)) + warnings.warn(msg) + # use either the EH or ET packet, they have the same content (only + # trigger stop time is not in EH) + if len(eh_packets): + eh = EHPacket(eh_packets[0]) + else: + eh = EHPacket(et_packets[0]) + # only C0, C2, 16, 32 encodings supported right now + if eh.data_format == b"C0": + encoding = 'C0' + elif eh.data_format == b"C2": + encoding = 'C2' + elif eh.data_format == b"16": + encoding = '16' + elif eh.data_format == b"32": + encoding = '32' + else: + msg = ("Reftek data encoding '{}' not implemented yet. Please " + "open an issue on GitHub and provide a small (< 50kb) " + "test file.").format(eh.data_format) + raise NotImplementedError(msg) + header = { + "unit_id": self._data['unit_id'][0], + "experiment_number": self._data['experiment_number'][0], + "network": network, + "station": (eh.station_name + + eh.station_name_extension).strip(), + "location": location, "sampling_rate": eh.sampling_rate, + "reftek130": eh._to_dict()} + delta = 1.0 / eh.sampling_rate + delta_nanoseconds = int(delta * 1e9) + inds_dt = data['packet_type'] == b"DT" + data_channels = np.unique(data[inds_dt]['channel_number']) + for channel_number in data_channels: + inds = data['channel_number'] == channel_number + # channel number of EH/ET packets also equals zero (one of the + # three unused bytes in the extended header of EH/ET packets) + inds &= data['packet_type'] == b"DT" + packets = data[inds] + + # split into contiguous blocks, i.e. find gaps. packet sequence + # was sorted already.. + endtimes = ( + packets[:-1]["time"] + + packets[:-1]["number_of_samples"].astype(np.int64) * + delta_nanoseconds) + # check if next starttime matches seamless to last chunk + # 1e-3 seconds == 1e6 nanoseconds is the smallest time + # difference reftek130 format can represent, so anything larger + # or equal means a gap/overlap. + time_diffs_milliseconds_abs = np.abs( + packets[1:]["time"] - endtimes) / 1000000 + gaps = time_diffs_milliseconds_abs >= 1 + if np.any(gaps): + gap_split_indices = np.nonzero(gaps)[0] + 1 + contiguous = np.array_split(packets, gap_split_indices) + else: + contiguous = [packets] + + for packets_ in contiguous: + starttime = packets_[0]['time'] + + if headonly: + sample_data = np.array([], dtype=np.int32) + npts = packets_["number_of_samples"].sum() + else: + if encoding in ('C0', 'C2'): + sample_data = _unpack_C0_C2_data(packets_, + encoding) + elif encoding in ('16', '32'): + # rt130 stores in big endian + dtype = {'16': '>i2', '32': '>i4'}[encoding] + # just fix endianness and use correct dtype + sample_data = np.require( + packets_['payload'], + requirements=['C_CONTIGUOUS']) + # either int16 or int32 + sample_data = sample_data.view(dtype) + # account for number of samples, i.e. some packets + # might not use the full payload size but have + # empty parts at the end that need to be cut away + number_of_samples_max = sample_data.shape[1] + sample_data = sample_data.flatten() + # go through packets starting at the back, + # otherwise indices of later packets would change + # while looping + for ind, num_samps in reversed([ + (ind, num_samps) for ind, num_samps in + enumerate(packets_["number_of_samples"]) + if num_samps != number_of_samples_max]): + # looping backwards we can easily find the + # start of each packet, since the earlier + # packets are still untouched and at maximum + # sample length in our big array with all + # packets + start_of_packet = ind * number_of_samples_max + start_empty_part = start_of_packet + num_samps + end_empty_part = (start_of_packet + + number_of_samples_max) + sample_data = np.delete( + sample_data, + slice(start_empty_part, end_empty_part)) + npts = len(sample_data) + + tr = Trace(data=sample_data, header=copy.deepcopy(header)) + # channel number is not included in the EH/ET packet + # payload, so add it to stats as well.. + tr.stats.reftek130['channel_number'] = channel_number + if headonly: + tr.stats.npts = npts + tr.stats.starttime = UTCDateTime(ns=starttime) + """ + if component codes were explicitly provided, use them + together with the stream label + if component_codes is not None: + tr.stats.channel = (eh.stream_name.strip() + + component_codes[channel_number]) + # otherwise check if channel code is set for the given + # channel (seems to be not the case usually) + elif eh.channel_code[channel_number] is not None: + tr.stats.channel = eh.channel_code[channel_number] + # otherwise fall back to using the stream label together + # with the number of the channel in the file (starting with + # 0, as Z-1-2 is common use for data streams not oriented + # against North) + else: + msg = ("No channel code specified in the data file " + "and no component codes specified. Using " + "stream label and number of channel in file as " + "channel codes.") + warnings.warn(msg) + tr.stats.channel = ( + eh.stream_name.strip() + str(channel_number)) + """ + DS = self._data['data_stream_number'][0] + 1 + if DS != 9: + tr.stats.channel = "DS%s-%s" % (DS, channel_number + 1) + else: + tr.stats.channel = "MP-%s" % (channel_number + 1) + # check if endtime of trace is consistent + t_last = packets_[-1]['time'] + npts_last = packets_[-1]['number_of_samples'] + try: + if not headonly: + assert npts == len(sample_data) + if npts_last: + assert tr.stats.endtime == UTCDateTime( + ns=t_last) + (npts_last - 1) * delta + if npts: + assert tr.stats.endtime == ( + tr.stats.starttime + (npts - 1) * delta) + except AssertionError: + msg = ("Reftek file has a trace with an inconsistent " + "endtime or number of samples. Please open an " + "issue on GitHub and provide your file for" + "testing.") + raise Reftek130Exception(msg) + st += tr + return st diff --git a/sohstationviewer/model/reftek/from_rt2ms/packet.py b/sohstationviewer/model/reftek/from_rt2ms/packet.py new file mode 100644 index 0000000000000000000000000000000000000000..c3ddb8865c877ef54805ca1bf72277623fe88f44 --- /dev/null +++ b/sohstationviewer/model/reftek/from_rt2ms/packet.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Suggested updates to obspy.io.reftek.packet: +- Change type of EH_PAYLOAD['sampling_rate'] from int to float. +- Add eh_et_info method to EHPacket class. This method compiles EH and ET info + and returns a list of strings used to write the rt2ms log file. + +Maeva Pourpoint IRIS/PASSCAL +""" + +import obspy.io.reftek.packet as obspy_rt130_packet + +from obspy import UTCDateTime +from obspy.io.reftek.util import (_decode_ascii, + _parse_long_time, + _16_tuple_ascii, + _16_tuple_int, + _16_tuple_float) +from sohstationviewer.model.reftek.from_rt2ms.soh_packet import Packet + + +class Reftek130UnpackPacketError(ValueError): + pass + + +# name, offset, length (bytes) and converter routine for EH/ET packet payload +EH_PAYLOAD = { + "trigger_time_message": (0, 33, _decode_ascii), + "time_source": (33, 1, _decode_ascii), + "time_quality": (34, 1, _decode_ascii), + "station_name_extension": (35, 1, _decode_ascii), + "station_name": (36, 4, _decode_ascii), + "stream_name": (40, 16, _decode_ascii), + "_reserved_2": (56, 8, _decode_ascii), + "sampling_rate": (64, 4, float), + "trigger_type": (68, 4, _decode_ascii), + "trigger_time": (72, 16, _parse_long_time), + "first_sample_time": (88, 16, _parse_long_time), + "detrigger_time": (104, 16, _parse_long_time), + "last_sample_time": (120, 16, _parse_long_time), + "channel_adjusted_nominal_bit_weights": (136, 128, _16_tuple_ascii), + "channel_true_bit_weights": (264, 128, _16_tuple_ascii), + "channel_gain_code": (392, 16, _16_tuple_ascii), + "channel_ad_resolution_code": (408, 16, _16_tuple_ascii), + "channel_fsa_code": (424, 16, _16_tuple_ascii), + "channel_code": (440, 64, _16_tuple_ascii), + "channel_sensor_fsa_code": (504, 16, _16_tuple_ascii), + "channel_sensor_vpu": (520, 96, _16_tuple_float), + "channel_sensor_units_code": (616, 16, _16_tuple_ascii), + "station_channel_number": (632, 48, _16_tuple_int), + "_reserved_3": (680, 156, _decode_ascii), + "total_installed_channels": (836, 2, int), + "station_comment": (838, 40, _decode_ascii), + "digital_filter_list": (878, 16, _decode_ascii), + "position": (894, 26, _decode_ascii), + "reftek_120": (920, 80, None)} + +obspy_rt130_packet.EH_PAYLOAD = EH_PAYLOAD + + +class EHPacket(obspy_rt130_packet.EHPacket): + def __str__(self, compact=False): + if compact: + sta = (self.station_name.strip() + + self.station_name_extension.strip()) + info = ("{:04d} {:2s} {:4s} {:2d} {:4d} {:4d} {:2d} {:2s} " + "{:5s} {:4s} {!s}").format( + self.packet_sequence, self.type.decode(), + self.unit_id.decode(), self.experiment_number, + self.byte_count, self.event_number, + self.data_stream_number, self.data_format.decode(), + sta, str(self.sampling_rate)[:4], self.time) + else: + info = [] + for key in self._headers: + value = getattr(self, key) + if key in ("unit_id", "data_format"): + value = value.decode() + info.append("{}: {}".format(key, value)) + info.append("-" * 20) + for key in sorted(EH_PAYLOAD.keys()): + value = getattr(self, key) + if key in ("trigger_time", "detrigger_time", + "first_sample_time", "last_sample_time"): + if value is not None: + value = UTCDateTime(ns=value) + info.append("{}: {}".format(key, value)) + info = "{} Packet\n\t{}".format(self.type.decode(), + "\n\t".join(info)) + return info + + def eh_et_info(self, nbr_DT_samples): + """ + Compile EH and ET info to write to log file. + Returns list of strings. + Formatting of strings is based on earlier version of rt2ms. + """ + info = [] + # packet_tagline1 = ("\n\n{:s} exp {:02d} bytes {:04d} {:s} ID: {:s} " + # "seq {:04d}".format(self.type.decode(), + # self.experiment_number, + # self.byte_count, + # Packet.time_tag(self.time), + # self.unit_id.decode(), + # self.packet_sequence)) + # info.append(packet_tagline1) + # if self.type.decode('ASCII') == 'EH': + # nbr_DT_samples = 0 + # info.append("\nEvent Header") + # else: + # info.append("\nEvent Trailer") + # info.append("\n event = " + str(self.event_number)) + # info.append("\n stream = " + str(self.data_stream_number + 1)) + # info.append("\n format = " + self.data_format.decode('ASCII')) + # info.append("\n stream name = " + self.stream_name) + # info.append("\n sample rate = " + str(self.sampling_rate)) + # info.append("\n trigger type = " + self.trigger_type) + trigger_time = Packet.time_tag(UTCDateTime(ns=self.trigger_time)) + # info.append("\n trigger time = " + trigger_time) + first_sample_time = Packet.time_tag(UTCDateTime(ns=self.first_sample_time)) # noqa: E501 + # info.append("\n first sample = " + first_sample_time) + # if self.last_sample_time: + # info.append("\n last sample = " + Packet.time_tag(UTCDateTime(ns=self.last_sample_time))) # noqa: E501 + # info.append("\n bit weights = " + " ".join([val for val in self.channel_adjusted_nominal_bit_weights if val])) # noqa: E501 + # info.append("\n true weights = " + " ".join([val for val in self.channel_true_bit_weights if val])) # noqa: E501 + packet_tagline2 = ("\nDAS: {:s} EV: {:04d} DS: {:d} FST = {:s} TT = " + "{:s} NS: {:d} SPS: {:.1f} ETO: 0" + .format(self.unit_id.decode(), + self.event_number, + self.data_stream_number + 1, + first_sample_time, + trigger_time, + nbr_DT_samples, + self.sampling_rate)) + info.append(packet_tagline2) + return info diff --git a/sohstationviewer/model/reftek/from_rt2ms/soh_packet.py b/sohstationviewer/model/reftek/from_rt2ms/soh_packet.py new file mode 100644 index 0000000000000000000000000000000000000000..ff54fe1539c53d352b8efa036bc66ed0f6c7eb0b --- /dev/null +++ b/sohstationviewer/model/reftek/from_rt2ms/soh_packet.py @@ -0,0 +1,928 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Routines building upon obspy.io.reftek.packet. +Redefine packet header (PACKET) based on rt130 manual. +Handles all state of health (SOH) packets: +- SH: State-Of-Health Packet +- SC: Station/Channel Parameter Packet +- OM: Operating Mode Parameter Packet +- DS: Data Stream Parameter Packet +- AD: Auxiliary Data Parameter Packet +- CD: Calibration Parameter Packet +- FD: Filter Description Packet + +Maeva Pourpoint IRIS/PASSCAL +""" + +import obspy.io.reftek.packet as obspy_rt130_packet +import numpy as np +import warnings + +from obspy import UTCDateTime +from obspy.core.compatibility import from_buffer +from obspy.io.reftek.util import (bcd, bcd_hex, _decode_ascii, + bcd_julian_day_string_to_nanoseconds_of_year, + bcd_16bit_int, _parse_long_time, + _get_nanoseconds_for_start_of_year) + + +class Reftek130UnpackPacketError(ValueError): + pass + + +AD_DS_RECORDING_DEST = ["RAM", "Disk", "Ethernet", "Serial"] +CD_MAX_NBR_STRUCT = 4 +DS_MAX_NBR_ST = 4 +SC_MAX_NBR_CHA = 5 + +# Packet header for rt130 state of health packets. +# Tuples are: +# - field name +# - dtype during initial reading +# - conversion routine (if any) +# - dtype after conversion +PACKET = [ + ("packet_type", "|S2", None, "S2"), + ("experiment_number", np.uint8, bcd, np.uint8), + ("year", np.uint8, bcd, np.uint8), + ("unit_id", (np.uint8, 2), bcd_hex, "S4"), + ("time", (np.uint8, 6), bcd_julian_day_string_to_nanoseconds_of_year, np.int64), # noqa: E501 + ("byte_count", (np.uint8, 2), bcd_16bit_int, np.uint16), + ("packet_sequence", (np.uint8, 2), bcd_16bit_int, np.uint16), + ("payload", (np.uint8, 1008), None, (np.uint8, 1008))] + +PACKET_INITIAL_UNPACK_DTYPE = np.dtype([(name, dtype_initial) + for name, dtype_initial, converter, + dtype_final in PACKET]) +PACKET_FINAL_DTYPE = np.dtype([(name, dtype_final) for name, dtype_initial, + converter, dtype_final in PACKET]) + +# name, length (bytes) and converter routine for packet payload +SH_PAYLOAD = { + "reserved": (8, _decode_ascii), + "information": (1000, _decode_ascii)} + +SC_PAYLOAD = { + "experiment_number_sc": (2, _decode_ascii), + "experiment_name": (24, _decode_ascii), + "experiment_comment": (40, _decode_ascii), + "station_number": (4, _decode_ascii), + "station_name": (24, _decode_ascii), + "station_comment": (40, _decode_ascii), + "das_model": (12, _decode_ascii), + "das_serial": (12, _decode_ascii), + "experiment_start": (14, _decode_ascii), + "time_clock_type": (4, _decode_ascii), + "time_clock_sn": (10, _decode_ascii), + "sc_info": (730, None), + "reserved": (76, _decode_ascii), + "implement_time": (16, _parse_long_time)} + +SC_INFO = { + "_number": (2, _decode_ascii), + "_name": (10, _decode_ascii), + "_azimuth": (10, _decode_ascii), + "_inclination": (10, _decode_ascii), + "_x_coordinate": (10, _decode_ascii), + "_y_coordinate": (10, _decode_ascii), + "_z_coordinate": (10, _decode_ascii), + "_xy_unit": (4, _decode_ascii), + "_z_unit": (4, _decode_ascii), + "_preamp_gain": (4, _decode_ascii), + "_sensor_model": (12, _decode_ascii), + "_sensor_serial": (12, _decode_ascii), + "_comments": (40, _decode_ascii), + "_adjusted_nominal_bit_weight": (8, _decode_ascii)} + +OM_PAYLOAD = { + "_72A_power_state": (2, _decode_ascii), + "recording_mode": (2, _decode_ascii), + "disk_reserved_1": (4, _decode_ascii), + "auto_dump_on_ET": (1, _decode_ascii), + "disk_reserved_2": (1, _decode_ascii), + "auto_dump_threshold": (2, _decode_ascii), + "_72A_power_down_delay": (4, _decode_ascii), + "disk_wrap": (1, _decode_ascii), + "disk_reserved_3": (1, _decode_ascii), + "_72A_disk_power": (1, _decode_ascii), + "_72A_terminator_power": (1, _decode_ascii), + "disk_retry": (1, _decode_ascii), + "disk_reserved_4": (11, _decode_ascii), + "_72A_wakeup_reserved_1": (2, _decode_ascii), + "_72A_wakeup_start_time": (12, _decode_ascii), + "_72A_wakeup_duration": (6, _decode_ascii), + "_72A_wakeup_repeat_interval": (6, _decode_ascii), + "_72A_wakeup_number_intervals": (2, _decode_ascii), + "_72A_wakeup_reserved_2": (484, _decode_ascii), + "reserved": (448, _decode_ascii), + "implement_time": (16, _parse_long_time)} + +DS_PAYLOAD = { + "ds_info": (920, None), + "reserved": (72, _decode_ascii), + "implement_time": (16, _parse_long_time)} + +DS_INFO = { + "_number": (2, _decode_ascii), + "_name": (16, _decode_ascii), + "_recording_destination": (4, _decode_ascii), + "_reserved_1": (4, _decode_ascii), + "_channels_included": (16, _decode_ascii), + "_sample_rate": (4, _decode_ascii), + "_data_format": (2, _decode_ascii), + "_reserved_2": (16, _decode_ascii), + "_trigger_type": (4, _decode_ascii), + "_trigger_description": (162, None)} + +DS_TRIGGER = { + "CON": { + "RecordLength": (8, _decode_ascii), + "StartTime": (14, _decode_ascii), + "Reserved": (140, _decode_ascii)}, + "CRS": { + "TriggerStreamNo": (2, _decode_ascii), + "PretriggerLength": (8, _decode_ascii), + "RecordLength": (8, _decode_ascii), + "Reserved": (144, _decode_ascii)}, + "EVT": { + "TriggerChannels": (16, _decode_ascii), + "MinimumChannels": (2, _decode_ascii), + "TriggerWindow": (8, _decode_ascii), + "PretriggerLength": (8, _decode_ascii), + "PosttriggerLength": (8, _decode_ascii), + "RecordLength": (8, _decode_ascii), + "Reserved1": (8, _decode_ascii), + "STALength": (8, _decode_ascii), + "LTALength": (8, _decode_ascii), + "MeanRemoval": (8, _decode_ascii), + "TriggerRatio": (8, _decode_ascii), + "DetriggerRatio": (8, _decode_ascii), + "LTAHold": (4, _decode_ascii), + "LowPassCornerFreq": (4, _decode_ascii), + "HighPassCornerFreq": (4, _decode_ascii), + "Reserved2": (52, _decode_ascii)}, + "EXT": { + "PretriggerLength": (8, _decode_ascii), + "RecordLength": (8, _decode_ascii), + "Reserved": (146, _decode_ascii)}, + "LEV": { + "Level": (8, _decode_ascii), + "PretriggerLength": (8, _decode_ascii), + "RecordLength": (8, _decode_ascii), + "LowPassCornerFreq": (4, _decode_ascii), + "HighPassCornerFreq": (4, _decode_ascii), + "Reserved": (130, _decode_ascii)}, + "TIM": { + "StartTime": (14, _decode_ascii), + "RepeatInterval": (8, _decode_ascii), + "Intervals": (4, _decode_ascii), + "Reserved1": (8, _decode_ascii), + "RecordLength": (8, _decode_ascii), + "Reserved2": (120, _decode_ascii)}, + "TML": { + "StartTime01": (14, _decode_ascii), + "StartTime02": (14, _decode_ascii), + "StartTime03": (14, _decode_ascii), + "StartTime04": (14, _decode_ascii), + "StartTime05": (14, _decode_ascii), + "StartTime06": (14, _decode_ascii), + "StartTime07": (14, _decode_ascii), + "StartTime08": (14, _decode_ascii), + "StartTime09": (14, _decode_ascii), + "StartTime10": (14, _decode_ascii), + "StartTime11": (14, _decode_ascii), + "RecordLength": (8, _decode_ascii)}} + +AD_PAYLOAD = { + "marker": (2, _decode_ascii), + "channels": (16, _decode_ascii), + "sample_period": (8, _decode_ascii), + "data_format": (2, _decode_ascii), + "record_length": (8, _decode_ascii), + "recording_destination": (4, _decode_ascii), + "reserved": (952, _decode_ascii), + "implement_time": (16, _parse_long_time)} + +CD_PAYLOAD = { + "_72A_start_time": (14, _decode_ascii), + "_72A_repeat_interval": (8, _decode_ascii), + "_72A_number_intervals": (4, _decode_ascii), + "_72A_length": (8, _decode_ascii), + "_72A_step_onoff": (4, _decode_ascii), + "_72A_step_period": (8, _decode_ascii), + "_72A_step_size": (8, _decode_ascii), + "_72A_step_amplitude": (8, _decode_ascii), + "_72A_step_output": (4, _decode_ascii), + "_72A_reserved": (48, _decode_ascii), + "_130_autocenter": (64, None), + "_130_signal": (112, None), + "_130_sequence": (232, None), + "reserved": (470, _decode_ascii), + "implement_time": (16, _parse_long_time)} + +CD_130_AUTOCENTER = { + "_sensor": (1, _decode_ascii), + "_enable": (1, _decode_ascii), + "_reading_interval": (4, _decode_ascii), + "_cycle_interval": (2, _decode_ascii), + "_level": (4, _decode_ascii), + "_attempts": (2, _decode_ascii), + "_attempts_interval": (2, _decode_ascii)} + +CD_130_SIGNAL = { + "_sensor": (1, _decode_ascii), + "_enable": (1, _decode_ascii), + "_reserved": (2, _decode_ascii), + "_duration": (4, _decode_ascii), + "_amplitude": (4, _decode_ascii), + "_signal": (4, _decode_ascii), + "_step_interval": (4, _decode_ascii), + "_step_width": (4, _decode_ascii), + "_sine_frequency": (4, _decode_ascii)} + +CD_130_SEQUENCE = { + "_sequence": (1, _decode_ascii), + "_enable": (1, _decode_ascii), + "_reserved_1": (2, _decode_ascii), + "_start_time": (14, _decode_ascii), + "_interval": (8, _decode_ascii), + "_count": (2, _decode_ascii), + "_record_length": (8, _decode_ascii), + "_sensor": (4, _decode_ascii), + "_reserved_2": (18, _decode_ascii)} + +FD_PAYLOAD = { + "fd_info": (992, None), + "implement_time": (16, _parse_long_time)} + +FD_INFO = { + "_filter_block_count": (1, int), + "_filter_ID": (1, _decode_ascii), + "_filter_decimation": (1, int), + "_filter_scalar": (1, int), + "_filter_coeff_count": (1, int), + "_packet_coeff_count": (1, int), + "_coeff_packet_count": (1, int), + "_coeff_format": (1, bcd), + "_coeff": (984, None)} + + +class Packet(obspy_rt130_packet.Packet): + """Class used to define shared tools for the SOH packets""" + + _headers = ('experiment_number', 'unit_id', 'byte_count', + 'packet_sequence', 'time') + + @staticmethod + def from_data(data): + """ + Checks for valid packet type identifier and returns appropriate + packet object + """ + packet_type = data['packet_type'].decode("ASCII", "ignore") + if packet_type == "SH": + return SHPacket(data) + elif packet_type == "SC": + return SCPacket(data) + elif packet_type == "OM": + return OMPacket(data) + elif packet_type == "DS": + return DSPacket(data) + elif packet_type == "AD": + return ADPacket(data) + elif packet_type == "CD": + return CDPacket(data) + elif packet_type == "FD": + return FDPacket(data) + else: + msg = "Can not create Reftek SOH packet for packet type '{}'" + raise NotImplementedError(msg.format(packet_type)) + + @staticmethod + def time_tag(time, implement_time=None): + if implement_time is not None and time > UTCDateTime(ns=implement_time): # noqa: E501 + time = UTCDateTime(ns=implement_time) + return "{:04d}:{:03d}:{:02d}:{:02d}:{:02d}:{:03d}".format(time.year, + time.julday, + time.hour, + time.minute, + time.second, + time.microsecond) # noqa: E501 + + @property + def packet_tagline(self): + return "\n" + # return "\n\n{:s} exp {:02d} bytes {:04d} {:s} ID: {:s} seq {:04d}".format(self.type.decode(), # noqa: E501 + # self.experiment_number, # noqa: E501 + # self.byte_count, # noqa: E501 + # self.time_tag(self.time), # noqa: E501 + # self.unit_id.decode(), # noqa: E501 + # self.packet_sequence) # noqa: E501 + + +class SHPacket(Packet): + """Class used to parse and generate string representation for SH packets""" + + def __init__(self, data): + self._data = data + payload = self._data["payload"].tobytes() + start_sh = 0 + for name, (length, converter) in SH_PAYLOAD.items(): + data = payload[start_sh:start_sh + length] + if converter is not None: + try: + data = converter(data) + except ValueError: + msg = ("SH packet, wrong conversion routine for input " + "variable: {}".format(name)) + warnings.warn(msg) + data = '' + setattr(self, name, data) + start_sh = start_sh + length + + def __str__(self): + info = [] + # info.append(self.packet_tagline) + packet_soh_string = ("\nState of Health {:s} ST: {:s}" + .format(self.time_tag(self.time)[2:], + self.unit_id.decode())) + info.append(packet_soh_string) + info.append("\n" + self.information.strip()) + return info + + +class SCPacket(Packet): + """Class used to parse and generate string representation for SC packets""" + + def __init__(self, data): + # Station/Channel payload + self._data = data + payload = self._data["payload"].tobytes() + start_sc = 0 + for name, (length, converter) in SC_PAYLOAD.items(): + data = payload[start_sc:start_sc + length] + if converter is not None: + try: + data = converter(data) + except ValueError: + msg = ("SC packet, wrong conversion routine for input " + "variable: {}".format(name)) + warnings.warn(msg) + data = '' + setattr(self, name, data) + start_sc = start_sc + length + # Detailed info for each channel - Up to 5 channels + start_info = 0 + for ind_sc in range(1, SC_MAX_NBR_CHA + 1): + for name, (length, converter) in SC_INFO.items(): + data = self.sc_info[start_info:start_info + length] + if converter is not None: + try: + data = converter(data) + except ValueError: + msg = ("SC packet, channel info, wrong conversion " + "routine for input variable : {}".format(name)) + warnings.warn(msg) + data = '' + name = "sc" + str(ind_sc) + name + setattr(self, name, data) + start_info = start_info + length + + def __str__(self): + info = [] + # info.append(self.packet_tagline) + # packet_soh_string = ("\nStation Channel Definition {:s} ST: {:s}" + # .format(self.time_tag(self.time, implement_time=self.implement_time), # noqa: E501 + # self.unit_id.decode())) + packet_soh_string = ("\nStation Channel Definition {:s} ST: {:s}" + .format(self.time_tag(self.time), + self.unit_id.decode())) + info.append(packet_soh_string) + info.append("\n Experiment Number = " + self.experiment_number_sc) + info.append("\n Experiment Name = " + self.experiment_name) + info.append("\n Comments - " + self.experiment_comment) + info.append("\n Station Number = " + self.station_number) + info.append("\n Station Name = " + self.station_name) + info.append("\n Station Comments - " + self.station_comment) + info.append("\n DAS Model Number = " + self.das_model) + info.append("\n DAS Serial Number = " + self.das_serial) + info.append("\n Experiment Start Time = " + self.experiment_start) + info.append("\n Time Clock Type = " + self.time_clock_type) + info.append("\n Clock Serial Number = " + self.time_clock_sn) + for ind_sc in range(1, SC_MAX_NBR_CHA + 1): + channel_number = getattr(self, 'sc' + str(ind_sc) + '_number') + if channel_number.strip(): + info.append("\n Channel Number = " + channel_number) + info.append("\n Name - " + getattr(self, 'sc' + str(ind_sc) + '_name')) # noqa: E501 + info.append("\n Azimuth - " + getattr(self, 'sc' + str(ind_sc) + '_azimuth')) # noqa: E501 + info.append("\n Inclination - " + getattr(self, 'sc' + str(ind_sc) + '_inclination')) # noqa: E501 + info.append("\n Location") + info.append("\n X - " + getattr(self, 'sc' + str(ind_sc) + '_x_coordinate')) # noqa: E501 + info.append(" Y - " + getattr(self, 'sc' + str(ind_sc) + '_y_coordinate')) # noqa: E501 + info.append(" Z - " + getattr(self, 'sc' + str(ind_sc) + '_z_coordinate')) # noqa: E501 + info.append("\n XY Units - " + getattr(self, 'sc' + str(ind_sc) + '_xy_unit')) # noqa: E501 + info.append(" Z Units - " + getattr(self, 'sc' + str(ind_sc) + '_z_unit')) # noqa: E501 + info.append("\n Preamplifier Gain = " + getattr(self, 'sc' + str(ind_sc) + '_preamp_gain')) # noqa: E501 + info.append("\n Sensor Model - " + getattr(self, 'sc' + str(ind_sc) + '_sensor_model')) # noqa: E501 + info.append("\n Sensor Serial Number - " + getattr(self, 'sc' + str(ind_sc) + '_sensor_serial')) # noqa: E501 + info.append("\n Volts per Bit = " + getattr(self, 'sc' + str(ind_sc) + '_adjusted_nominal_bit_weight')) # noqa: E501 + info.append("\n Comments - " + getattr(self, 'sc' + str(ind_sc) + '_comments')) # noqa: E501 + return info + + def get_info(self, infos): + """ + Compile relevant information - unit id, reference channel, network + code, station code, component code, gain and implementation time - for + given SC packet + """ + implement_time = UTCDateTime(ns=self.implement_time) + for ind_sc in range(1, SC_MAX_NBR_CHA + 1): + channel_number = getattr(self, 'sc' + str(ind_sc) + '_number') + if channel_number.strip(): + gain = getattr(self, 'sc' + str(ind_sc) + '_preamp_gain') + if gain is None: + msg = ("No gain available for parameter packet " + "implemented at {} - refchan {}" + .format(implement_time, channel_number)) + warnings.warn(msg) + continue + # order: #das, refchan, netcode, station, channel[3], + # gain, implement_time + info = [self.unit_id.decode(), + channel_number.strip(), + self.experiment_number_sc.strip(), + self.station_name.strip(), + getattr(self, 'sc' + str(ind_sc) + '_name').strip(), + gain.strip(), + implement_time] + if info not in infos: + infos.append(info) + return infos + + +class OMPacket(Packet): + """Class used to parse and generate string representation for OM packets""" + + def __init__(self, data): + self._data = data + payload = self._data["payload"].tobytes() + start_om = 0 + for name, (length, converter) in OM_PAYLOAD.items(): + data = payload[start_om:start_om + length] + if converter is not None: + try: + data = converter(data) + except ValueError: + msg = ("OM packet, wrong conversion routine for input " + "variable: {}".format(name)) + warnings.warn(msg) + data = '' + setattr(self, name, data) + start_om = start_om + length + + def __str__(self): + info = [] + # info.append(self.packet_tagline) + packet_soh_string = ("\nOperating Mode Definition {:s} ST: {:s}" + .format(self.time_tag(self.time, implement_time=self.implement_time), # noqa: E501 + self.unit_id.decode())) + info.append(packet_soh_string) + info.append("\n Operating Mode 72A Power State " + self._72A_power_state) # noqa: E501 + info.append("\n Operating Mode Recording Mode " + self.recording_mode) + info.append("\n Operating Mode Auto Dump on ET " + self.auto_dump_on_ET) # noqa: E501 + info.append("\n Operating Mode Auto Dump Threshold " + self.auto_dump_threshold) # noqa: E501 + info.append("\n Operating Mode 72A Power Down Delay " + self._72A_power_down_delay) # noqa: E501 + info.append("\n Operating Mode Disk Wrap " + self.disk_wrap) + info.append("\n Operating Mode 72A Disk Power " + self._72A_disk_power) # noqa: E501 + info.append("\n Operating Mode 72A Terminator Power " + self._72A_terminator_power) # noqa: E501 + info.append("\n Operating Mode 72A Wake Up Start Time " + self._72A_wakeup_start_time) # noqa: E501 + info.append("\n Operating Mode 72A Wake Up Duration " + self._72A_wakeup_duration) # noqa: E501 + info.append("\n Operating Mode 72A Wake Up Repeat Interval " + self._72A_wakeup_repeat_interval) # noqa: E501 + info.append("\n Operating Mode 72A Number of Wake Up Intervals " + self._72A_wakeup_number_intervals) # noqa: E501 + return info + + +class DSPacket(Packet): + """Class used to parse and generate string representation for DS packets""" + + def __init__(self, data): + # Data Stream payload + self._data = data + payload = self._data["payload"].tobytes() + start_ds = 0 + for name, (length, converter) in DS_PAYLOAD.items(): + data = payload[start_ds:start_ds + length] + if converter is not None: + try: + data = converter(data) + except ValueError: + msg = ("DS packet, wrong conversion routine for input " + "variable: {}".format(name)) + warnings.warn(msg) + data = '' + setattr(self, name, data) + start_ds = start_ds + length + # Detailed info for each stream - Up to 4 streams + start_info = 0 + for ind_ds in range(1, DS_MAX_NBR_ST + 1): + for name, (length, converter) in DS_INFO.items(): + data = self.ds_info[start_info:start_info + length] + if converter is not None: + try: + data = converter(data) + except ValueError: + msg = ("DS packet, data stream info, wrong conversion " + "routine for input variable: {}".format(name)) + warnings.warn(msg) + data = '' + name = "ds" + str(ind_ds) + name + setattr(self, name, data) + start_info = start_info + length + # Detailed info for each trigger + trigger_type = getattr(self, "ds" + str(ind_ds) + "_trigger_type").strip() # noqa: E501 + if trigger_type in DS_TRIGGER: + start_trigger = 0 + for name, (length, converter) in DS_TRIGGER[trigger_type].items(): # noqa: E501 + data = getattr(self, "ds" + str(ind_ds) + "_trigger_description")[start_trigger:start_trigger + length] # noqa: E501 + if converter is not None: + try: + data = converter(data) + except ValueError: + msg = ("DS packet, trigger info, wrong conversion " + "routine for input variable: {}" + .format(name)) + warnings.warn(msg) + data = '' + name = "ds" + str(ind_ds) + "_trigger_" + name + setattr(self, name, data) + start_trigger = start_trigger + length + elif trigger_type.strip(): + msg = ("Trigger type {:s} not found".format(trigger_type)) + warnings.warn(msg) + + def __str__(self): + info = [] + info.append(self.packet_tagline) + packet_soh_string = ("\nData Stream Definition {:s} ST: {:s}" + .format(self.time_tag(self.time, implement_time=self.implement_time), # noqa: E501 + self.unit_id.decode())) + info.append(packet_soh_string) + for ind_ds in range(1, DS_MAX_NBR_ST + 1): + stream_number = getattr(self, "ds" + str(ind_ds) + "_number") + if stream_number.strip(): + recording_dest = [AD_DS_RECORDING_DEST[ind_rd] + for ind_rd, val in enumerate(getattr(self, "ds" + str(ind_ds) + "_recording_destination")) # noqa: E501 + if val.strip()] + info.append(" ".join(["\n Data Stream", + stream_number, + getattr(self, "ds" + str(ind_ds) + "_name"), # noqa: E501 + ", ".join(recording_dest)])) + channels = getattr(self, "ds" + str(ind_ds) + "_channels_included") # noqa: E501 + channel_nbr = [str(ind_chan) for ind_chan, val in enumerate(channels, 1) if val.strip()] # noqa: E501 + info.append("\n Channels " + ", ".join(channel_nbr)) + info.append("\n Sample rate " + getattr(self, "ds" + str(ind_ds) + "_sample_rate") + " samples per second") # noqa: E501 + info.append("\n Data Format " + getattr(self, "ds" + str(ind_ds) + "_data_format")) # noqa: E501 + trigger_type = getattr(self, "ds" + str(ind_ds) + "_trigger_type").strip() # noqa: E501 + info.append("\n Trigger Type " + trigger_type) + if trigger_type in DS_TRIGGER: + for key in DS_TRIGGER[trigger_type].keys(): + if "reserved" not in key.lower(): + trigger_info = getattr(self, "ds" + str(ind_ds) + "_trigger_" + key) # noqa: E501 + if trigger_info: + if trigger_type == "EVT" and key == "TriggerChannels": # noqa: E501 + channel_nbr = [str(ind_chan) for ind_chan, val in enumerate(trigger_info, 1) if val.strip()] # noqa: E501 + info.append(" ".join(["\n Trigger", key, ", ".join(channel_nbr)])) # noqa: E501 + else: + info.append(" ".join(["\n Trigger", key, trigger_info])) # noqa: E501 + return info + + def get_info(self, infos): + """ + Compile relevant information - reference data stream, band and + instrument codes, sample rate and implementation time - for given DS + packet + """ + implement_time = UTCDateTime(ns=self.implement_time) + for ind_ds in range(1, DS_MAX_NBR_ST + 1): + stream_number = getattr(self, "ds" + str(ind_ds) + "_number") + if stream_number.strip(): + samplerate = getattr(self, "ds" + str(ind_ds) + "_sample_rate") + if samplerate is None: + msg = ("No sampling rate available for parameter packet " + "implemented at {} - refstrm {}" + .format(implement_time, stream_number)) + warnings.warn(msg) + continue + # order: refstrm, channel[0:2], samplerate, implement_time + info = [stream_number.strip(), + getattr(self, "ds" + str(ind_ds) + "_name").strip(), + samplerate.strip(), + implement_time] + if info not in infos: + infos.append(info) + return infos + + +class ADPacket(Packet): + """Class used to parse and generate string representation for AD packets""" + + def __init__(self, data): + self._data = data + payload = self._data["payload"].tobytes() + start_ad = 0 + for name, (length, converter) in AD_PAYLOAD.items(): + data = payload[start_ad:start_ad + length] + if converter is not None: + try: + data = converter(data) + except ValueError: + msg = ("AD packet, wrong conversion routine for input " + "variable: {}".format(name)) + warnings.warn(msg) + data = '' + setattr(self, name, data) + start_ad = start_ad + length + + def __str__(self): + info = [] + # info.append(self.packet_tagline) + packet_soh_string = ("\nAuxiliary Data Parameter {:s} ST: {:s}" + .format(self.time_tag(self.time, implement_time=self.implement_time), # noqa: E501 + self.unit_id.decode())) + info.append(packet_soh_string) + channel_nbr = [str(ind_chan) for ind_chan, val in + enumerate(self.channels, 1) if val.strip()] + info.append("\n Channels " + ", ".join(channel_nbr)) + info.append("\n Sample Period " + self.sample_period) + info.append("\n Data Format " + self.data_format) + info.append("\n Record Length " + self.record_length) + recording_dest = [AD_DS_RECORDING_DEST[ind_rd] for ind_rd, val + in enumerate(self.recording_destination) + if val.strip()] + info.append("\n Recording Destination " + ", ".join(recording_dest)) + return info + + +class CDPacket(Packet): + """Class used to parse and generate string representation for CD packets""" + + def __init__(self, data): + # Calibration parameter payload + self._data = data + payload = self._data["payload"].tobytes() + start_cd = 0 + for name, (length, converter) in CD_PAYLOAD.items(): + data = payload[start_cd:start_cd + length] + if converter is not None: + try: + data = converter(data) + except ValueError: + msg = ("CD packet, wrong conversion routine for input " + "variable: {}".format(name)) + warnings.warn(msg) + data = '' + setattr(self, name, data) + start_cd = start_cd + length + + start_info_ac = 0 + start_info_sig = 0 + start_info_seq = 0 + for ind_cd in range(1, CD_MAX_NBR_STRUCT + 1): + # Detailed info for 130 Sensor Auto-Center - Up to 4 structures + for name, (length, converter) in CD_130_AUTOCENTER.items(): + data = self._130_autocenter[start_info_ac:start_info_ac + length] # noqa: E501 + if converter is not None: + try: + data = converter(data) + except ValueError: + msg = ("CD packet, auto-center info, wrong conversion " + "routine for input variable: {}".format(name)) + warnings.warn(msg) + data = '' + name = "cd_130_autocenter_" + str(ind_cd) + name + setattr(self, name, data) + start_info_ac = start_info_ac + length + # Detailed info for 130 Sensor Calibration Signal + # Up to 4 structures + for name, (length, converter) in CD_130_SIGNAL.items(): + data = self._130_signal[start_info_sig:start_info_sig + length] + if converter is not None: + try: + data = converter(data) + except ValueError: + msg = ("CD packet, calibration signal info, wrong " + "conversion routine for input variable: {}" + .format(name)) + warnings.warn(msg) + data = '' + name = "cd_130_signal_" + str(ind_cd) + name + setattr(self, name, data) + start_info_sig = start_info_sig + length + # Detailed info for 130 Sensor Calibration Sequence + # Up to 4 structures + for name, (length, converter) in CD_130_SEQUENCE.items(): + data = self._130_sequence[start_info_seq:start_info_seq + length] # noqa: E501 + if converter is not None: + try: + data = converter(data) + except ValueError: + msg = ("CD packet, calibration sequence info, wrong " + "conversion routine for input variable: {}" + .format(name)) + warnings.warn(msg) + data = '' + name = "cd_130_sequence_" + str(ind_cd) + name + setattr(self, name, data) + start_info_seq = start_info_seq + length + + def __str__(self): + info = [] + # info.append(self.packet_tagline) + packet_soh_string = ("\nCalibration Definition {:s} ST: {:s}" + .format(self.time_tag(self.time, implement_time=self.implement_time), # noqa: E501 + self.unit_id.decode())) + info.append(packet_soh_string) + + if self._72A_start_time.split(): + info.append("\n 72A Calibration Start Time " + self._72A_start_time) # noqa: E501 + info.append("\n 72A Calibration Repeat Interval " + self._72A_repeat_interval) # noqa: E501 + info.append("\n 72A Calibration Intervals " + self._72A_number_intervals) # noqa: E501 + info.append("\n 72A Calibration Length " + self._72A_length) + info.append("\n 72A Calibration Step On/Off " + self._72A_step_onoff) # noqa: E501 + info.append("\n 72A Calibration Step Period " + self._72A_step_period) # noqa: E501 + info.append("\n 72A Calibration Step Size " + self._72A_step_size) + info.append("\n 72A Calibration Step Amplitude " + self._72A_step_amplitude) # noqa: E501 + info.append("\n 72A Calibration Step Output " + self._72A_step_output) # noqa: E501 + + for ind_cd in range(1, CD_MAX_NBR_STRUCT + 1): + autocenter_sensor = getattr(self, "cd_130_autocenter_" + str(ind_cd) + '_sensor') # noqa: E501 + if autocenter_sensor.strip(): + info.append("\n 130 Auto Center Sensor " + autocenter_sensor) + info.append("\n 130 Auto Center Enable " + getattr(self, "cd_130_autocenter_" + str(ind_cd) + '_enable')) # noqa: E501 + info.append("\n 130 Auto Center Reading Interval " + getattr(self, "cd_130_autocenter_" + str(ind_cd) + '_reading_interval')) # noqa: E501 + info.append("\n 130 Auto Center Cycle Interval " + getattr(self, "cd_130_autocenter_" + str(ind_cd) + '_cycle_interval')) # noqa: E501 + info.append("\n 130 Auto Center Level " + getattr(self, "cd_130_autocenter_" + str(ind_cd) + '_level')) # noqa: E501 + info.append("\n 130 Auto Center Attempts " + getattr(self, "cd_130_autocenter_" + str(ind_cd) + '_attempts')) # noqa: E501 + info.append("\n 130 Auto Center Attempt Interval " + getattr(self, "cd_130_autocenter_" + str(ind_cd) + '_attempts_interval')) # noqa: E501 + + for ind_cd in range(1, CD_MAX_NBR_STRUCT + 1): + signal_sensor = getattr(self, "cd_130_signal_" + str(ind_cd) + '_sensor') # noqa: E501 + if signal_sensor.strip(): + info.append("\n 130 Calibration Sensor " + signal_sensor) + info.append("\n 130 Calibration Enable " + getattr(self, "cd_130_signal_" + str(ind_cd) + '_enable')) # noqa: E501 + info.append("\n 130 Calibration Duration " + getattr(self, "cd_130_signal_" + str(ind_cd) + '_duration')) # noqa: E501 + info.append("\n 130 Calibration Amplitude " + getattr(self, "cd_130_signal_" + str(ind_cd) + '_amplitude')) # noqa: E501 + info.append("\n 130 Calibration Signal " + getattr(self, "cd_130_signal_" + str(ind_cd) + '_signal')) # noqa: E501 + info.append("\n 130 Calibration Step Interval " + getattr(self, "cd_130_signal_" + str(ind_cd) + '_step_interval')) # noqa: E501 + info.append("\n 130 Calibration Step Width " + getattr(self, "cd_130_signal_" + str(ind_cd) + '_step_width')) # noqa: E501 + info.append("\n 130 Calibration Sine Frequency " + getattr(self, "cd_130_signal_" + str(ind_cd) + '_sine_frequency')) # noqa: E501 + + for ind_cd in range(1, CD_MAX_NBR_STRUCT + 1): + sequence_sensor = getattr(self, "cd_130_sequence_" + str(ind_cd) + '_sensor') # noqa: E501 + if sequence_sensor.strip(): + info.append("\n 130 Calibration Sequence " + sequence_sensor) + info.append("\n 130 Calibration Sequence Enable " + getattr(self, "cd_130_sequence_" + str(ind_cd) + '_enable')) # noqa: E501 + info.append("\n 130 Calibration Sequence Start Time " + getattr(self, "cd_130_sequence_" + str(ind_cd) + '_start_time')) # noqa: E501 + info.append("\n 130 Calibration Sequence Interval " + getattr(self, "cd_130_sequence_" + str(ind_cd) + '_interval')) # noqa: E501 + info.append("\n 130 Calibration Sequence Count " + getattr(self, "cd_130_sequence_" + str(ind_cd) + '_count')) # noqa: E501 + info.append("\n 130 Calibration Sequence Record Length " + getattr(self, "cd_130_sequence_" + str(ind_cd) + '_record_length')) # noqa: E501 + return info + + +class FDPacket(Packet): + """Class used to parse and generate string representation for FD packets""" + + def __init__(self, data): + # Filter description payload + self._data = data + payload = self._data["payload"] + start_fd = 0 + for name, (length, converter) in FD_PAYLOAD.items(): + data = payload[start_fd:start_fd + length] + if converter is not None and data.size != 0: + try: + data = converter(data.tobytes()) + except ValueError: + msg = ("FD packet, wrong conversion routine for input " + "variable: {}".format(name)) + warnings.warn(msg) + data = '' + setattr(self, name, data) + start_fd = start_fd + length + # Detailed info for filter block(s) (fb) + # Warning - rt130 manual section on FD packets is not very clear. + # The following code needs to be further tested if FD packets + # contains several filter blocks or if packet coefficient count + # in a given filter block is superior to 248 32-bit coefficients. + # Test data including these scenarios is however lacking. + start_info = 0 + nbr_fbs = self.fd_info[0] + 1 + setattr(self, 'nbr_fbs', nbr_fbs) + if nbr_fbs > 1: + msg = ("FD packet contains more than one filter block - " + "Handling of additional block has not been fully tested") + warnings.warn(msg) + for ind_fb in range(1, nbr_fbs + 1): + for name, (length, converter) in FD_INFO.items(): + if name == "_coeff": + coeff_size = int(getattr(self, "fb" + str(ind_fb) + "_coeff_format")[0] / 8) # noqa: E501 + setattr(self, "fb" + str(ind_fb) + '_coeff_size', coeff_size) # noqa: E501 + length = getattr(self, "fb" + str(ind_fb) + "_packet_coeff_count") * coeff_size # noqa: E501 + data = self.fd_info[start_info:start_info + length] + if converter is not None: + if converter is _decode_ascii: + data = data.tobytes() + try: + data = converter(data) + except ValueError: + msg = ("FD packet, filter block info, wrong " + "conversion routine for input variable: {}" + .format(name)) + warnings.warn(msg) + data = '' + name = "fb" + str(ind_fb) + name + setattr(self, name, data) + start_info = start_info + length + + def __str__(self): + info = [] + # info.append(self.packet_tagline) + packet_soh_string = ("\nFilter Description {:s} ST: {:s}" + .format(self.time_tag(self.time, implement_time=self.implement_time), # noqa: E501 + self.unit_id.decode())) + info.append(packet_soh_string) + for ind_fb in range(1, self.nbr_fbs + 1): + info.append("\n Filter Block Count " + str(getattr(self, "fb" + str(ind_fb) + "_filter_block_count"))) # noqa: E501 + info.append("\n Filter ID " + getattr(self, "fb" + str(ind_fb) + "_filter_ID")) # noqa: E501 + info.append("\n Filter Decimation " + str(getattr(self, "fb" + str(ind_fb) + "_filter_decimation"))) # noqa: E501 + info.append("\n Filter Scalar " + str(getattr(self, "fb" + str(ind_fb) + "_filter_scalar"))) # noqa: E501 + info.append("\n Filter Coefficient Count " + str(getattr(self, "fb" + str(ind_fb) + "_filter_coeff_count"))) # noqa: E501 + info.append("\n Filter Packet Coefficient Count " + str(getattr(self, "fb" + str(ind_fb) + "_packet_coeff_count"))) # noqa: E501 + info.append("\n Filter Coefficient Packet Count " + str(getattr(self, "fb" + str(ind_fb) + "_coeff_packet_count"))) # noqa: E501 + info.append("\n Filter Coefficient Format " + str(getattr(self, "fb" + str(ind_fb) + "_coeff_format")[0])) # noqa: E501 + info.append("\n Filter Coefficients:") + start_coeff = 0 + for inf_coeff in range(0, getattr(self, "fb" + str(ind_fb) + "_packet_coeff_count")): # noqa: E501 + length = getattr(self, "fb" + str(ind_fb) + '_coeff_size') # noqa: E501 + coeff = getattr(self, "fb" + str(ind_fb) + "_coeff")[start_coeff:start_coeff + length] # noqa: E501 + start_coeff = start_coeff + length + twosCom_bin = ''.join([self.twosCom_dec2bin(x, 8) for x in coeff]) # noqa: E501 + coeff_dec = self.twosCom_bin2dec(twosCom_bin, length * 8) + info.append(" " + str(coeff_dec)) + return info + + @staticmethod + def twosCom_bin2dec(bin_, digit): + while len(bin_) < digit: + bin_ = '0' + bin_ + if bin_[0] == '0': + return int(bin_, 2) + else: + return -1 * (int(''.join('1' if x == '0' else '0' for x in bin_), 2) + 1) # noqa: E501 + + @staticmethod + def twosCom_dec2bin(dec, digit): + if dec >= 0: + bin_ = bin(dec).split("0b")[1] + while len(bin_) < digit: + bin_ = '0' + bin_ + return bin_ + else: + bin_ = -1 * dec + return bin(dec - pow(2, digit)).split("0b")[1] + + +def _initial_unpack_packets_soh(bytestring): + """ + First unpack data with dtype matching itemsize of storage in the reftek + file, than allocate result array with dtypes for storage of python + objects/arrays and fill it with the unpacked data. + """ + if not len(bytestring): + return np.array([], dtype=PACKET_FINAL_DTYPE) + + if len(bytestring) % 1024 != 0: + tail = len(bytestring) % 1024 + bytestring = bytestring[:-tail] + msg = ("Length of data not a multiple of 1024. Data might be " + "truncated. Dropping {:d} byte(s) at the end.").format(tail) + warnings.warn(msg) + data = from_buffer( + bytestring, dtype=PACKET_INITIAL_UNPACK_DTYPE) + result = np.empty_like(data, dtype=PACKET_FINAL_DTYPE) + + for name, dtype_initial, converter, dtype_final in PACKET: + if converter is None: + result[name][:] = data[name][:] + else: + try: + result[name][:] = converter(data[name]) + except Exception as e: + raise Reftek130UnpackPacketError(str(e)) + # time unpacking is special and needs some additional work. + # we need to add the POSIX timestamp of the start of respective year to the + # already unpacked seconds into the respective year.. + result['time'][:] += [_get_nanoseconds_for_start_of_year(y) + for y in result['year']] + return result diff --git a/sohstationviewer/model/reftek/logInfo.py b/sohstationviewer/model/reftek/logInfo.py new file mode 100644 index 0000000000000000000000000000000000000000..1947dbe7adf2717999bbe1feab5e0f36cb86f81d --- /dev/null +++ b/sohstationviewer/model/reftek/logInfo.py @@ -0,0 +1,406 @@ + +from sohstationviewer.conf import constants +from sohstationviewer.controller.util import ( + displayTrackingInfo, getTime6, getTime4, getVal, + rtnPattern) + + +class LogInfo(): + def __init__(self, parent, parentGUI, logText, key, packetType, reqDSs, + isLogFile=False): + self.packetType = packetType + self.parent = parent + self.parentGUI = parentGUI + self.logText = logText + self.key = key + self.unitID, self.expNo = key + self.reqDSs = reqDSs + self.isLogFile = isLogFile + """ + trackYear to add year to time since year time not include year after + header. + yearChanged: if doy=1, trackYear is added 1, yAdded is marked + this has happened to only added 1 once. + """ + self.yAdded = False + self.trackYear = 0 + if self.unitID >= "9000": + self.model = "RT130" + else: + self.model = "72A" + self.maxEpoch = 0 + self.minEpoch = constants.HIGHEST_INT + self.chans = self.parent.plottingData[self.key]['channels'] + self.CPUVers = set() + self.GPSVers = set() + self.extractInfo() + + def readEVT(self, line): + # Ex: DAS: 0108 EV: 2632 DS: 2 FST = 2001:253:15:13:59:768 + # TT =2001:253:15:13:59:768 NS: 144005 SPS: 40 ETO: 0 + parts = line.split() + DS = int(parts[5]) + if DS not in self.parent.reqDSs: + return (0, 0) + try: + if parts[8].startswith("00:000"): + if parts[11].startswith("00:000"): + return -1, 0 + epoch, _ = getTime6(parts[11]) + else: + epoch, _ = getTime6(parts[8]) + except AttributeError: + self.parent.processingLog.append(line, 'error') + return False + if epoch > 0: + self.minEpoch = min(epoch, self.minEpoch) + self.maxEpoch = max(epoch, self.maxEpoch) + else: + return 0, 0 + return epoch, DS + + def readSHHeader(self, line): + # Ex: State of Health 01:251:09:41:35:656 ST: 0108 + parts = line.split() + try: + epoch, self.trackYear = getTime6(parts[3]) + except AttributeError: + self.parent.processingLog.append(line, 'error') + return False + self.yAdded = False # reset yAdded + self.minEpoch = min(epoch, self.minEpoch) + self.maxEpoch = max(epoch, self.maxEpoch) + unitID = parts[5].strip() + if unitID != self.unitID: + msg = ("The State Of Health messages for DAS %s were being " + "read, but now there is information for DAS %s in " + "the same State Of Health messages.\n" + "Skip reading State Of Health." % (self.unitID, unitID)) + displayTrackingInfo(self.parentGUI, msg, 'error') + False + return epoch + + def simpleRead(self, line): + # Ex: 186:21:41:35 <content> + parts = line.split() + try: + epoch, self.trackYear, self.yAdded = getTime4( + parts[0], self.trackYear, self.yAdded) + except AttributeError: + self.parent.processingLog.append(line, 'error') + return False + self.maxEpoch = max(epoch, self.maxEpoch) + return parts, epoch + + def readIntClockPhaseErr(self, line): + # Ex: 253:19:41:42 INTERNAL CLOCK PHASE ERROR OF 4823 USECONDS + ret = self.simpleRead(line) + if not ret: + return False + parts, epoch = ret + error = float(parts[-2]) + # if parts[-1].startswith("USEC"): bc requested unit is us + # error /= 1000.0 + if parts[-1].startswith("SEC"): + error *= 1000000.0 + return epoch, error + + def readBatTemBkup(self, line): + # 72A's: + # Ex: 186:14:33:58 BATTERY VOLTAGE = 13.6V, TEMPERATURE = 26C + # RT130: + # Ex: 186:14:33:58 BATTERY VOLTAGE = 13.6V, TEMPERATURE = 26C, + # BACKUP = 3.3V + parts, epoch = self.simpleRead(line) + try: + volts = getVal(parts[4]) + temp = getVal(parts[7]) + except AttributeError: + self.parent.processingLog.append(line, 'error') + return False + if self.model == "RT130": + bkupV = getVal(parts[10]) + else: + bkupV = 0.0 + return epoch, volts, temp, bkupV + + def readDiskUsage(self, line): + # RT130: + # Ex: 186:14:33:58 DISK 1: USED: 89744 AVAIL:... + # Ex: 186:14:33:58 DISK 2* USED: 89744 AVAIL:... + parts, epoch = self.simpleRead(line) + try: + disk = getVal(parts[2]) + val = getVal(parts[4]) + except AttributeError: + self.parent.processingLog.append(line, 'error') + return False + return epoch, disk, val + + def readDPS_ClockDiff(self, line): + # Ex: 245:07:41:45 DSP CLOCK DIFFERENCE: 0 SECS AND -989 MSECS + parts, epoch = self.simpleRead(line) + try: + secs = getVal(parts[4]) + msecs = getVal(parts[-2]) + except AttributeError: + self.parent.processingLog.append(line, 'error') + return False + total = abs(secs) * 1000.0 + abs(msecs) + if secs < 0.0 or msecs < 0.0: + total *= -1.0 + return epoch, total + + def readDefs(self, line): + # Ex: Station Channel Definition 01:330:19:24:42:978 ST: 7095 + # Ex: Data Stream Definition 01:330:19:24:42:978 ST: 7095 + # Ex: Calibration Definition 01:330:19:24:42:978 ST: 7095 + # Ex: STATION CHANNEL DEFINITION 2020:066:19:00:56:000 ST: 92E9 + parts = line.split() + # Lines from a .log file may be just the first time the parameters were + # saved until the DAS is reset, so if this is a log file then use the + # current SOH time for plotting the points, instead of what is in the + # message line. + # if parts[0] in ["STATION", "DATA", "CALIBRATION"]: + # TODO: check if want to plot other def + if parts[0] in ["STATION"]: + if self.isLogFile is False: + try: + epoch, _ = getTime6(parts[-3]) + except AttributeError: + self.parent.processingLog.append(line, 'error') + return False + else: + epoch = self.maxEpoch + else: + return False + # These will not be allowed to set TimeMin since the RT130's save + # multiple copies of the parameters in the log files (one per day), + # but they all have the date/timestamp of the first time the parameters + # were written (unless a new copy is written because the parameters + # were changed, of course). + return epoch + + def readCPUVer(self, line): + # Ex: 341:22:05:41 CPU SOFTWARE V03.00H (72A and older 130 FW) + # Ex: 341:22:05:41 REF TEK 130 (no version number at all) + # Ex: 341:22:05:41 Ref Tek 130 2.8.8S (2007:163) + # Ex: 341:22:05:41 CPU SOFTWARE V 3.0.0 (2009:152) + parts = line.split() + if parts[1].startswith("CPU"): + CPUVer = " ".join(parts[3:]) + elif parts[1].upper().startswith("REF"): + # There may not be any version info in this line: + # Ex: REF TEK 130...then nothing + # but accessing non-existant InParts doesn't matter. + # CPUVer will just be "". + CPUVer = " ".join(parts[4:]) + return CPUVer + + def readGPSVer(self, line): + parts = line.split() + verParts = [p.strip() for p in parts] + if "GPS FIRMWARE VERSION:" in line: + # 130 Ex: 291:19:54:29 GPS FIRMWARE VERSION: GPS 16-HVS Ver. 3.20 + GPSVer = ' '.join(verParts[4:]) + else: + # 72A: Ex: 159:15:41:05 GPS: V03.30 (17JAN2000) + GPSVer = " ".join(verParts[2:]) + return GPSVer + + def addChanInfo(self, chanName, t, d, idx): + if chanName not in self.chans: + self.chans[chanName] = { + 'unitID': self.unitID, + 'expNo': self.expNo, + 'times': [], + 'data': [], + 'logIdx': []} + self.chans[chanName]['times'].append(t) + self.chans[chanName]['data'].append(d) + self.chans[chanName]['logIdx'].append(idx) + + def extractInfo(self): + + lines = [ln.strip() for ln in self.logText.splitlines() if ln != ''] + sohEpoch = 0 + + for idx, line in enumerate(lines): + line = line.upper() + if 'FST' in line: + ret = self.readEVT(line) + if ret: + epoch, DS = ret + if DS in self.reqDSs: + if epoch > 0: + chanName = 'Event DS%s' % DS + self.addChanInfo(chanName, epoch, 1, idx) + elif epoch == 0: + self.parent.processingLog.append(line, 'warning') + else: + self.parent.processingLog.append(line, 'error') + + elif line.startswith("STATE OF HEALTH"): + epoch = self.readSHHeader(line) + if epoch: + self.addChanInfo('SOH/Data Def', epoch, 1, idx) + elif "DEFINITION" in line: + epoch = self.readDefs(line) + if epoch: + self.addChanInfo('SOH/Data Def', epoch, 0, idx) + + if "INTERNAL CLOCK PHASE ERROR" in line: + ret = self.readIntClockPhaseErr(line) + if ret: + epoch, err = ret + self.addChanInfo('Clk Phase Err', epoch, err, idx) + + elif "POSSIBLE DISCREPANCY" in line: + epoch = self.simpleRead(line)[1] + if epoch: + self.addChanInfo('Discrepancies', epoch, 1, idx) + + elif "BATTERY VOLTAGE" in line: + ret = self.readBatTemBkup(line) + if ret: + epoch, volts, temp, bkupV = ret + self.addChanInfo('Battery Volt', epoch, volts, idx) + self.addChanInfo('DAS Temp', epoch, temp, idx) + # bkupV: x<3, 3<=x<3.3, >3.3 + self.addChanInfo('Backup Volt', epoch, bkupV, idx) + + elif all(x in line for x in ['DISK', 'USED']): + ret = self.readDiskUsage(line) + if ret: + epoch, disk, val = ret + self.addChanInfo(f'Disk Usage{int(disk)}', epoch, val, idx) + + elif "AUTO DUMP CALLED" in line: + epoch = self.simpleRead(line)[1] + if epoch: + self.addChanInfo('Dump Called/Comp', epoch, 1, idx) + elif "AUTO DUMP COMPLETE" in line: + epoch = self.simpleRead(line)[1] + if epoch: + self.addChanInfo('Dump Called/Comp', epoch, 0, idx) + + elif "DSP CLOCK SET" in line: + epoch = self.simpleRead(line)[1] + if epoch: + self.addChanInfo('Jerks/DSP Sets', epoch, 1, idx) + elif 'JERK' in line: + epoch = self.simpleRead(line)[1] + if epoch: + self.addChanInfo('Jerks/DSP Sets', epoch, 0, idx) + + elif "DPS clock diff" in line: + ret = self.readDPS_ClockDiff() + if ret: + epoch, total = ret + self.addChanInfo('DPS Clock Diff', epoch, total, idx) + + elif "ACQUISITION STARTED" in line: + epoch = self.simpleRead(line)[1] + if epoch: + self.addChanInfo('ACQ On/Off', epoch, 1, idx) + elif "ACQUISITION STOPPED" in line: + epoch = self.simpleRead(line)[1] + if epoch: + self.addChanInfo('ACQ On/Off', epoch, 0, idx) + + elif "NETWORK LAYER IS UP" in line: + epoch = self.simpleRead(line)[1] + if epoch: + self.addChanInfo('Net Up/Down', epoch, 0, idx) + elif "NETWORK LAYER IS DOWN" in line: + epoch = self.simpleRead(line)[1] + if epoch: + self.addChanInfo('Net Up/Down', epoch, 1, idx) + + elif "MASS RE-CENTER" in line: + epoch = self.simpleRead(line)[1] + if epoch: + self.addChanInfo('Mass Re-center', epoch, idx) + + elif any(x in line for x in ["SYSTEM RESET", "FORCE RESET"]): + epoch = self.simpleRead(line)[1] + if epoch: + self.addChanInfo('Reset/Power-up', epoch, 0, idx) + elif "SYSTEM POWERUP" in line: + epoch = self.simpleRead(line)[1] + if epoch: + self.addChanInfo('Reset/Power-up', epoch, 1, idx) + + # ================= GPS ================================== + elif "EXTERNAL CLOCK IS UNLOCKED" in line: + epoch = self.simpleRead(line)[1] + if epoch: + self.addChanInfo('GPS Lk/Unlk', epoch, 0, idx) + elif "EXTERNAL CLOCK IS LOCKED" in line: + epoch = self.simpleRead(line)[1] + if epoch: + self.addChanInfo('GPS Lk/Unlk', epoch, 1, idx) + + elif any(x in line for x in ["EXTERNAL CLOCK POWER IS TURNED ON", + "EXTERNAL CLOCK WAKEUP", + "GPS: POWER IS TURNED ON", + "GPS: POWER IS CONTINUOUS"] + ): + epoch = self.simpleRead(line)[1] + if epoch: + self.addChanInfo('GPS On/Off/Err', epoch, 1, idx) + elif any(x in line for x in ["EXTERNAL CLOCK POWER IS TURNED OFF", + "EXTERNAL CLOCK SLEEP", + "GPS: POWER IS TURNED OFF", + "NO EXTERNAL CLOCK"] + ): + epoch = self.simpleRead(line)[1] + if epoch: + self.addChanInfo('GPS On/Off/Err', epoch, 0, idx) + elif "EXTERNAL CLOCK ERROR" in line: + epoch = self.simpleRead(line)[1] + if epoch: + self.addChanInfo('GPS On/Off/Err', epoch, -1, idx) + + elif "EXTERNAL CLOCK CYCLE" in line: + epoch = self.simpleRead(line)[1] + if epoch: + self.addChanInfo('GPS Clock Power', epoch, 1, idx) + + # ================= VERSIONS ============================== + elif any(x in line for x in ["REF TEK", "CPU SOFTWARE"]): + CPUVer = self.readCPUVer(line) + self.CPUVers.add(CPUVer) + + elif any(x in line for x in ["GPS FIRMWARE VERSION:", "GPS: V"]): + GPSVer = self.readGPSVer(line) + self.GPSVers.add(GPSVer) + + # ================= ERROR/WARNING ========================= + elif "ERROR:" in line: + # These lines are generated by programs like ref2segy and do + # not have any time associated with them so just use whatever + # time was last in Time. + self.addChanInfo('Error/Warning', sohEpoch, -2, idx) + elif any(x in line for x in ["NO VALID DISK", "FAILED"]): + epoch = self.simpleRead(line)[1] + if epoch: + self.addChanInfo('Error/Warning', epoch, -1, idx) + + elif "WARNING" in line: + # Warings come in lines with the time from the RT130 and lines + # created by programs without times. + if rtnPattern(line[:12]) == "000:00:00:00": + epoch = self.simpleRead(line)[1] + if epoch: + self.addChanInfo('Error/Warning', epoch, 0, idx) + else: + # Just use whatever time was last in Time. + # The 2 means that the dots will be a little different. + self.addChanInfo('Error/Warning', sohEpoch, 1, idx) + + elif (all(x in line for x in ["BAD", "MAKER"]) or + any(x in line for x in ["ERTFS", "IDE BUSY"])): + epoch = self.simpleRead(line)[1] + if epoch: + self.addChanInfo('Error/Warning', epoch, 0, idx) diff --git a/sohstationviewer/model/reftek/reftek.py b/sohstationviewer/model/reftek/reftek.py new file mode 100755 index 0000000000000000000000000000000000000000..48e3a4e891c4f1e88e579f61c8da5fcff220cb45 --- /dev/null +++ b/sohstationviewer/model/reftek/reftek.py @@ -0,0 +1,210 @@ + +import os +import numpy as np + +from obspy import UTCDateTime +from obspy.core import Stream + +from sohstationviewer.model.reftek.from_rt2ms import ( + core, soh_packet, packet) +from sohstationviewer.model.reftek.logInfo import LogInfo +from sohstationviewer.conf import constants + +from sohstationviewer.model.dataType import DataType + + +class RT130(DataType): + def __init__(self, *args, **kwarg): + self.EH = {} + super().__init__(*args, **kwarg) + + def readFile(self, path, fileName): + if not self.readReftek130(path, fileName): + self.readText(path, fileName) + + def readReftek130(self, path, fileName): + path2file = os.path.join(path, fileName) + rt130 = core.Reftek130.from_file(path2file) + unique, counts = np.unique(rt130._data["packet_type"], + return_counts=True) + nbr_packet_type = dict(zip(unique, counts)) + + if b"SH" in nbr_packet_type: + self.readSH(path2file) + if b"EH" in nbr_packet_type or b"ET" in nbr_packet_type: + self.readEHET(rt130) + return True + + def readEHET(self, rt130): + DS = rt130._data['data_stream_number'][0] + 1 + if DS not in self.reqDSs + [9]: + return + + ind_EHET = [ind for ind, val in + enumerate(rt130._data["packet_type"]) + if val in [b"EH"]] # on ly need event header + nbr_DT_samples = sum( + [rt130._data[ind]["number_of_samples"] + for ind in range(0, len(rt130._data)) + if ind not in ind_EHET]) + + for ind in ind_EHET: + d = rt130._data[ind] + self.curKey = (d['unit_id'].decode(), + d['experiment_number']) + if self.curKey not in self.logData: + self.logData[self.curKey] = {} + logs = packet.EHPacket(d).eh_et_info(nbr_DT_samples) + self.addLog('EHET', (d['time'], logs)) + + stream = core.Reftek130.to_stream( + rt130, + headonly=False, + verbose=False, + sort_permuted_package_sequence=True) + for trace in stream: + k = (d['unit_id'].decode(), d['experiment_number']) + if k not in self.streams.keys(): + self.streams[self.curKey] = Stream() + self.streams[k].append(trace) + + def readSH(self, path2file): + with open(path2file, "rb") as fh: + str = fh.read() + data = soh_packet._initial_unpack_packets_soh(str) + for ind, d in enumerate(data): + self.curKey = (d['unit_id'].decode(), + d['experiment_number']) + if self.curKey not in self.logData: + self.logData[self.curKey] = {} + logs = soh_packet.Packet.from_data(d).__str__() + # self.addLog(d['packet_type'].decode(), (d['time'], logs)) + self.addLog('SOH', (d['time'], logs)) + + def readTrace(self, trace, channels): + chan = {} + chan['netID'] = trace.stats['network'] + chan['statID'] = trace.stats['station'] + chan['locID'] = trace.stats['location'] + chanID = chan['chanID'] = trace.stats['channel'] + + chan['samplerate'] = trace.stats['sampling_rate'] + chan['startTmEpoch'] = trace.stats['starttime'].timestamp + chan['endTmEpoch'] = trace.stats['endtime'].timestamp + """ + trace time start with 0 => need to add with epoch starttime + """ + chan['times'] = trace.times() + trace.stats['starttime'].timestamp + chan['data'] = trace.data + if chanID.startswith('MP'): + # calculation based on Logpeek's rt130MPDecode() + # TODO: MP only has 4 different values, data can be simplified + # by removing data point with same values in a row + # after converting, the variety of values for MP even reduce more + chan['data'] = np.round_(chan['data'] / 3276.7, 1) + + if chanID not in channels.keys(): + channels[chanID] = chan + else: + msg = ("Something wrong with code logic." + "After stream is merged there should be only one trace " + "for channel %s: %s" % (chanID, trace)) + self.trackInfo(msg, "error") + return trace.stats['starttime'], trace.stats['endtime'] + + def combineData(self): + for k in self.logData: + self.plottingData[k] = { + 'gaps': [], + 'channels': {} + } + logs = [] + for pktType in ['SOH', 'EHET']: + if pktType == 'EHET': + logs += '\n\nEvents:' + try: + for log in sorted(self.logData[k][pktType], + key=lambda x: x[0]): + # sort log data according to time + logs += log[1] + if pktType == 'SOH': + logs += '\n' + except KeyError: + pass + logStr = ''.join(logs) + self.logData[k][pktType] = logStr + logObj = LogInfo( + self, self.parentGUI, logStr, k, pktType, self.reqDSs) + self.plottingData[k]['earliestUTC'] = logObj.minEpoch + self.plottingData[k]['latestUTC'] = logObj.maxEpoch + for cName in self.plottingData[k]['channels']: + c = self.plottingData[k]['channels'][cName] + c['times'] = np.array(c['times']) + c['data'] = np.array(c['data']) + c['logIdx'] = np.array(c['logIdx']) + + for k in self.streams: + stream = self.streams[k] + stream.merge() + # gaps is list of [network, station, location, channel, starttime, + # endtime, duration, number of missing samples] + gaps = stream.get_gaps() + # stream.print_gaps() + gaps = self.squashGaps(gaps) + if k not in self.plottingData: + self.plottingData[k] = {'gaps': gaps, + 'channels': {}} + maxEndtime = UTCDateTime(0) + minStarttime = UTCDateTime(20E10) + for trace in stream: + startTm, endTm = self.readTrace( + trace, self.plottingData[k]['channels']) + if startTm < minStarttime: + minStarttime = startTm + if endTm > maxEndtime: + maxEndtime = endTm + self.plottingData[k]['earliestUTC'] = min( + minStarttime.timestamp, + self.plottingData[k]['earliestUTC']) + self.plottingData[k]['latestUTC'] = max( + maxEndtime.timestamp, + self.plottingData[k]['latestUTC']) + + def squashGaps(self, gaps): + """ + Squash different lists of gaps for all channels to be one list of + (minStarttime, maxEndtime) gaps + : param gaps: list of [network, station, location, channel, starttime, + endtime, duration, number of missing samples] + : return squashedGaps + """ + if len(gaps) == 0: + return gaps + + # create dict consist of list of gaps for each channel + gaps_dict = {} + for g in gaps: + if g[3] not in gaps_dict: + gaps_dict[g[3]] = [] + gaps_dict[g[3]].append((g[4].timestamp, g[5].timestamp)) + + firstLen = len(gaps_dict[gaps[0][3]]) + diffLenList = [len(gaps_dict[k]) for k in gaps_dict.keys() + if len(gaps_dict[k]) != firstLen] + if len(diffLenList) > 0: + msg = "Number of Gaps for different channel are not equal.\n" + for k in gaps_dict.keys(): + msg += "%s: %s\n" % (k, len(gaps_dict[k])) + self.trackInfo(msg, 'error') + return [] + squashedGaps = [] + for i in range(firstLen): + maxEndtime = 0 + minStarttime = constants.HIGHEST_INT + for val in gaps_dict.values(): + if val[i][0] < minStarttime: + minStarttime = val[i][0] + if val[i][1] > maxEndtime: + maxEndtime = val[i][1] + squashedGaps.append((minStarttime, maxEndtime)) + return squashedGaps diff --git a/sohstationviewer/sohstationviewer.py b/sohstationviewer/sohstationviewer.py deleted file mode 100644 index d0c9153db95f19821308220463d5f97cf7ed16b1..0000000000000000000000000000000000000000 --- a/sohstationviewer/sohstationviewer.py +++ /dev/null @@ -1,12 +0,0 @@ -import sys -from PySide2 import QtWidgets - - -def main(): - app = QtWidgets.QApplication() - # gui = MainGUI - sys.exit(app.exec_()) - - -if __name__ == '__main__': - main() diff --git a/sohstationviewer/view/calendardialog.py b/sohstationviewer/view/calendardialog.py index 0303bce3a98955f7759085a1e5b7ab11435b1120..afb18249a3ae8f3fe3ffad6941f1913f4c2d5246 100644 --- a/sohstationviewer/view/calendardialog.py +++ b/sohstationviewer/view/calendardialog.py @@ -1,6 +1,6 @@ from PySide2 import QtWidgets -from sohstationviewer.view.ui.calendar_ui import Ui_CalendarDialog +from sohstationviewer.view.ui.calendar_ui_qtdesigner import Ui_CalendarDialog class CalendarDialog(QtWidgets.QDialog, Ui_CalendarDialog): diff --git a/sohstationviewer/view/channeldialog.py b/sohstationviewer/view/channeldialog.py new file mode 100755 index 0000000000000000000000000000000000000000..205f701d65d7738a4c7f4467a024b9c96db360d3 --- /dev/null +++ b/sohstationviewer/view/channeldialog.py @@ -0,0 +1,88 @@ +""" +channeldialog.py +GUI to add/edit/remove channels +""" + +from sohstationviewer.view.core.dbgui_superclass import Ui_DBInfoDialog +from sohstationviewer.database.proccessDB import executeDB + + +class ChannelDialog(Ui_DBInfoDialog): + def __init__(parent, self): + super().__init__( + parent, ['No.', 'Channel', 'Label', 'Param', + 'ConvertFactor', 'Unit', 'FixPoint'], + 'channel', 'channels', resizeContentColumns=[0, 4, 5, 6], + needDataTypeChoice=True, requestedColumns={3: 'Param'}, + checkFK=False) + self.setWindowTitle("Edit/Add/Delete Channels") + + def updateDataTableWidgetItems(self): + paraRows = executeDB("SELECT param from parameters") + self.paramChoices = [''] + sorted([d[0] for d in paraRows]) + super(ChannelDialog, self).updateDataTableWidgetItems() + + def clearFirstRow(self): + """ + device with no channels yet, there will be empty channel left + """ + self.dataTableWidget.cellWidget(0, 1).setText('') + self.dataTableWidget.cellWidget(0, 2).setText('') + self.dataTableWidget.cellWidget(0, 3).setCurrentIndex(-1) + self.dataTableWidget.cellWidget(0, 4).setText('1') + self.dataTableWidget.cellWidget(0, 5).setText('') + self.dataTableWidget.cellWidget(0, 6).setValue(0) + + def addRow(self, rowidx, fk=False): + self.addWidget(None, rowidx, 0) # No. + self.addWidget(self.dataList, rowidx, 1, foreignkey=fk) # chanID + self.addWidget(self.dataList, rowidx, 2) # label + self.addWidget(self.dataList, rowidx, 3, choices=self.paramChoices) + self.addWidget(self.dataList, rowidx, 4, + fieldName='convertFactor') + self.addWidget(self.dataList, rowidx, 5) # unit + self.addWidget(self.dataList, rowidx, 6, + range=[0, 5]) # fixPoint + + def dataTypeChanged(self): + self.dataType = self.dataTypeCombobox.currentText() + self.updateDataTableWidgetItems() + + def getDataList(self): + channelRows = executeDB( + f"SELECT channel, label, param, convertFactor, unit, fixPoint " + f"FROM Channels " + f"WHERE dataType='{self.dataType}'") + return [[d[0], d[1], d[2], d[3], + '' if d[4] is None else d[4], + d[5]] + for d in channelRows] + + def getRowInputs(self, rowidx): + return [ + self.dataTableWidget.cellWidget(rowidx, 1).text().strip(), + self.dataTableWidget.cellWidget(rowidx, 2).text().strip(), + self.dataTableWidget.cellWidget(rowidx, 3).currentText().strip(), + float(self.dataTableWidget.cellWidget(rowidx, 4).text()), + self.dataTableWidget.cellWidget(rowidx, 5).text(), + self.dataTableWidget.cellWidget(rowidx, 6).value() + ] + + def removeRow(self, removeRowidx): + self.dataTableWidget.removeRow(removeRowidx) + for i in range(removeRowidx, self.dataTableWidget.rowCount()): + cellWget = self.dataTableWidget.cellWidget(i, 0) + cellWget.setText(str(i)) + + def updateData(self, row, widgetidx, listidx): + insertsql = (f"INSERT INTO Channels VALUES" + f"('{row[0]}', '{row[1]}', '{row[2]}'," + f" {row[3]}, '{row[4]}', {row[5]}, '{self.dataType}')") + updatesql = (f"UPDATE Channels SET channel='{row[0]}', " + f"label='{row[1]}', param='{row[2]}', " + f"convertFactor={row[3]}, unit='{row[4]}' " + f"fixPoint={row[5]} " + f"WHERE channel='%s'" + f" AND dataType='{self.dataType}'") + return super().updateData( + row, widgetidx, listidx, insertsql, updatesql) diff --git a/sohstationviewer/view/channelpreferdialog.py b/sohstationviewer/view/channelpreferdialog.py new file mode 100755 index 0000000000000000000000000000000000000000..c246e70dac29fc99fada0590b57401d986a14168 --- /dev/null +++ b/sohstationviewer/view/channelpreferdialog.py @@ -0,0 +1,299 @@ +from PySide2 import QtWidgets, QtCore + +from sohstationviewer.database.proccessDB import ( + executeDB, trunc_addDB, executeDB_dict) +from sohstationviewer.controller.processing import readChannels, detectDataType +from sohstationviewer.controller.util import displayTrackingInfo + +INSTRUCTION = """ +Place lists of channels to be read in the IDs field.\n +Select the radiobutton for the list to be used in plotting. +""" +TOTAL_ROW = 20 + +COL = {'sel': 0, 'name': 1, 'dataType': 2, 'IDs': 3, 'clr': 4} + + +class ChannelPreferDialog(QtWidgets.QWidget): + def __init__(self, parent, dirnames): + super(ChannelPreferDialog, self).__init__() + self.parent = parent + self.dirnames = dirnames + self.setWindowTitle("Channel Preferences") + self.setGeometry(100, 100, 1100, 800) + mainLayout = QtWidgets.QVBoxLayout() + mainLayout.setContentsMargins(7, 7, 7, 7) + self.setLayout(mainLayout) + + mainLayout.addWidget(QtWidgets.QLabel(INSTRUCTION)) + + self.createIDsTableWidget() + mainLayout.addWidget(self.IDsTableWidget, 1) + + buttonLayout = self.createButtonsSection() + mainLayout.addLayout(buttonLayout) + + self.trackingInfoTextBrowser = QtWidgets.QTextBrowser(self) + self.trackingInfoTextBrowser.setFixedHeight(60) + mainLayout.addWidget(self.trackingInfoTextBrowser) + self.changed = False + + def createButtonsSection(self): + hLayout = QtWidgets.QHBoxLayout() + self.addDBChanBtn = QtWidgets.QPushButton( + self, text='Add DB Channels') + self.addDBChanBtn.clicked.connect(self.addDBChannels) + hLayout.addWidget(self.addDBChanBtn) + + self.scanChanBtn = QtWidgets.QPushButton( + self, text='Scan Channels from Data Source') + self.scanChanBtn.clicked.connect(self.scanChannels) + hLayout.addWidget(self.scanChanBtn) + + self.saveBtn = QtWidgets.QPushButton(self, text='Save') + self.saveBtn.clicked.connect(self.save) + hLayout.addWidget(self.saveBtn) + + self.save_addMainBtn = QtWidgets.QPushButton( + self, text='Save - Add to Main') + self.save_addMainBtn.clicked.connect(self.save_addToMainNClose) + hLayout.addWidget(self.save_addMainBtn) + + self.closeBtn = QtWidgets.QPushButton(self, text='Cancel') + self.closeBtn.clicked.connect(self.close) + hLayout.addWidget(self.closeBtn) + return hLayout + + def createIDsTableWidget(self): + self.IDsTableWidget = QtWidgets.QTableWidget(self) + # self.IDsTableWidget.verticalHeader().hide() + self.IDsTableWidget.setColumnCount(5) + colHeaders = ['', 'Name', 'DataType', 'IDs', 'Clear'] + self.IDsTableWidget.setHorizontalHeaderLabels(colHeaders) + header = self.IDsTableWidget.horizontalHeader() + header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) + header.setSectionResizeMode(3, QtWidgets.QHeaderView.Stretch) + + self.IDsTableWidget.setRowCount(TOTAL_ROW) + self.availDataTypes = self.getDataTypes() + for rowidx in range(TOTAL_ROW): + self.addRow(rowidx) + + self.currRow = -1 + self.updateDataTableWidgetItems() + + @QtCore.Slot() + def addRow(self, rowidx): + currSelRadioBtn = QtWidgets.QRadioButton(self) + currSelRadioBtn.clicked.connect( + lambda checked: self.currSelChanged(rowidx)) + self.IDsTableWidget.setCellWidget( + rowidx, COL['sel'], currSelRadioBtn) + + nameLineEdit = QtWidgets.QLineEdit(self) + nameLineEdit.textChanged.connect(self.inputChanged) + self.IDsTableWidget.setCellWidget( + rowidx, COL['name'], nameLineEdit) + + dataTypeCombobox = QtWidgets.QComboBox(self) + dataTypeCombobox.currentIndexChanged.connect(self.inputChanged) + dataTypeCombobox.addItems(['Unknown'] + self.availDataTypes) + dataTypeCombobox.setCurrentIndex(-1) + self.IDsTableWidget.setCellWidget( + rowidx, COL['dataType'], dataTypeCombobox) + + IDsLineEdit = QtWidgets.QLineEdit(self) + IDsLineEdit.textChanged.connect(self.inputChanged) + self.IDsTableWidget.setCellWidget( + rowidx, COL['IDs'], IDsLineEdit) + + delButton = QtWidgets.QPushButton(self, text='CLR') + delButton.clicked.connect(lambda arg: self.clearIDs(rowidx)) + self.IDsTableWidget.setCellWidget( + rowidx, COL['clr'], delButton) + + @QtCore.Slot() + def inputChanged(self): + self.changed = True + + def updateDataTableWidgetItems(self): + IDsRows = self.getIDsRows() + # first row + self.IDsTableWidget.cellWidget(0, COL['sel']).setChecked(True) + self.currSelChanged(0) + count = 0 + for r in IDsRows: + self.IDsTableWidget.cellWidget( + count, COL['sel']).setChecked( + True if ['current'] == 1 else False) + self.IDsTableWidget.cellWidget( + count, COL['name']).setText(r['name']) + self.IDsTableWidget.cellWidget( + count, COL['dataType']).setCurrentText(r['dataType']) + self.IDsTableWidget.cellWidget( + count, COL['IDs']).setText(r['IDs']) + self.IDsTableWidget.cellWidget( + count, COL['sel']).setChecked( + True if r['current'] == 1 else False) + if ['current'] == 1: + self.currSelChanged(count) + count += 1 + self.update() + + def getRow(self, rowidx): + nameWget = self.IDsTableWidget.cellWidget(rowidx, COL['name']) + dataTypeWget = self.IDsTableWidget.cellWidget(rowidx, COL['dataType']) + IDsWget = self.IDsTableWidget.cellWidget(rowidx, COL['IDs']) + clearWget = self.IDsTableWidget.cellWidget(rowidx, COL['clr']) + return (nameWget, dataTypeWget, IDsWget, clearWget) + + @QtCore.Slot() + def currSelChanged(self, rowidx): + self.currRow = rowidx + (self.nameWget, self.dataTypeWget, + self.IDsWget, self.clearWget) = self.getRow(rowidx) + self.inputChanged() + + @QtCore.Slot() + def clearIDs(self, rowidx): + (nameWget, dataTypeWget, IDsWget, clearWget) = self.getRow(rowidx) + if IDsWget.text().strip() != "": + msg = ("Are you sure you want to delete the channel IDs of " + "row #%s?" % (rowidx+1)) + result = QtWidgets.QMessageBox.question( + self, "Confirmation", msg, + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) + if result == QtWidgets.QMessageBox.No: + return + self.changed = True + if nameWget.text() != '': + self.changed = True + nameWget.setText('') + if dataTypeWget.currentText != '': + self.changed = True + dataTypeWget.setCurrentIndex(-1) + if IDsWget.text() != '': + self.changed = True + IDsWget.setText('') + + def validateRow(self, checkDataType=False): + if self.currRow == -1: + msg = ("Please select a row.") + QtWidgets.QMessageBox.information(self, "Select row", msg) + return False + + if checkDataType: + self.dataType = self.dataTypeWget.currentText() + if self.dataType not in self.availDataTypes: + msg = ("Please select a data type that isn't 'Unknown' for " + "the selected row.") + QtWidgets.QMessageBox.information( + self, "Select data type", msg) + return False + # # check IDs + # if self.IDsWget.text().strip() != '': + # msg = ("The selected row's IDs will be overwritten.\n" + # "Do you want to continue?") + # result = QtWidgets.QMessageBox.question( + # self, "Confirmation", msg, + # QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) + # if result == QtWidgets.QMessageBox.No: + # return False + return True + + @QtCore.Slot() + def addDBChannels(self): + if not self.validateRow(checkDataType=True): + return + + dbChannels = self.getDBChannels(self.dataType) + self.IDsWget.setText(','.join(dbChannels)) + + @QtCore.Slot() + def scanChannels(self): + if not self.validateRow(): + return + + dataType = detectDataType(self, self.dirnames) + if dataType in self.availDataTypes: + self.dataTypeWget.setCurrentText(dataType) + else: + self.dataTypeWget.setCurrenText('Unknown') + scannedChannels = readChannels(self, self.dirnames) + self.IDsWget.setText(','.join(scannedChannels)) + + @QtCore.Slot() + def save(self): + if not self.validateRow(): + return + if self.changed: + msg = ("All IDs in the database will be overwritten with " + "current IDs in the dialog.\nClick Cancel to stop updating " + "database.") + result = QtWidgets.QMessageBox.question( + self, "Confirmation", msg, + QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel) + if result == QtWidgets.QMessageBox.Cancel: + return False + sqlList = [] + for rowidx in range(TOTAL_ROW): + sql = self.saveRowSql(rowidx) + if sql is not None: + sqlList.append(sql) + if len(sqlList) == 0: + self.parent.IDs = [] + self.parent.IDsName = '' + self.parent.dataType = 'Unknown' + return True + + ret = trunc_addDB('ChannelPrefer', sqlList) + if ret is not True: + displayTrackingInfo(self.parent, ret, "error") + self.parent.IDs = [ + t.strip() for t in self.IDsWget.text().split(',')] + self.parent.IDsName = self.nameWget.text().strip() + self.parent.IDsDataType = self.dataTypeWget.currentText() + return True + + def saveRowSql(self, rowidx): + current = 1 if self.IDsTableWidget.cellWidget( + rowidx, COL['sel']).isChecked() else 0 + name = self.IDsTableWidget.cellWidget( + rowidx, COL['name']).text() + dataType = self.IDsTableWidget.cellWidget( + rowidx, COL['dataType']).currentText() + IDs = self.IDsTableWidget.cellWidget( + rowidx, COL['IDs']).text() + if IDs.strip() == '': + return + if name.strip() == '' and IDs.strip() != '': + msg = f"Please add Name for row {rowidx}." + QtWidgets.QMessageBox.information(self, "Missing info", msg) + return + return(f"INSERT INTO ChannelPrefer (name, IDs, dataType, current)" + f"VALUES ('{name}', '{IDs}', '{dataType}', {current})") + + @QtCore.Slot() + def save_addToMainNClose(self): + if not self.save(): + return + self.parent.currIDsNameLineEdit.setText(self.parent.IDsName) + self.parent.allChanCheckBox.setChecked(False) + self.close() + + def getDataTypes(self): + dataTypeRows = executeDB( + 'SELECT * FROM DataTypes ORDER BY dataType ASC') + return [d[0] for d in dataTypeRows] + + def getDBChannels(self, dataType): + channelRows = executeDB( + f"SELECT channel FROM CHANNELS WHERE dataType='{dataType}' " + f" ORDER BY dataType ASC") + return [c[0] for c in channelRows] + + def getIDsRows(self): + IDsRows = executeDB_dict( + "SELECT name, IDs, dataType, current FROM ChannelPrefer " + " ORDER BY name ASC") + return IDsRows diff --git a/sohstationviewer/view/core/__init__.py b/sohstationviewer/view/core/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/sohstationviewer/view/calendarwidget.py b/sohstationviewer/view/core/calendarwidget.py similarity index 100% rename from sohstationviewer/view/calendarwidget.py rename to sohstationviewer/view/core/calendarwidget.py diff --git a/sohstationviewer/view/core/dbgui_superclass.py b/sohstationviewer/view/core/dbgui_superclass.py new file mode 100755 index 0000000000000000000000000000000000000000..f24870287d8dbe3c415f9133a922cb75cb44c675 --- /dev/null +++ b/sohstationviewer/view/core/dbgui_superclass.py @@ -0,0 +1,367 @@ +from PySide2 import QtWidgets, QtGui + +from sohstationviewer.database.proccessDB import executeDB + + +def setWidgetColor(widget, changed=False, readOnly=False): + pallete = QtGui.QPalette() + + if readOnly: + # grey text + pallete.setColor(QtGui.QPalette.Text, QtGui.QColor(100, 100, 100)) + # light blue background + pallete.setColor(QtGui.QPalette.Base, QtGui.QColor(210, 240, 255)) + widget.setReadOnly(True) + widget.setPalette(pallete) + return + else: + # white background + pallete.setColor(QtGui.QPalette.Base, QtGui.QColor(255, 255, 255)) + if changed: + # red text + pallete.setColor(QtGui.QPalette.Text, QtGui.QColor(255, 0, 0)) + else: + try: + if widget.isReadOnly(): + # grey text + pallete.setColor( + QtGui.QPalette.Text, QtGui.QColor(100, 100, 100)) + else: + # black text + pallete.setColor(QtGui.QPalette.Text, QtGui.QColor(0, 0, 0)) + except AttributeError: + pallete.setColor(QtGui.QPalette.Text, QtGui.QColor(0, 0, 0)) + widget.setPalette(pallete) + + +class Ui_DBInfoDialog(QtWidgets.QWidget): + def __init__(self, parent, columnHeaders, colName, tableName, + resizeContentColumns=[], requestedColumns={}, + needDataTypeChoice=False, checkFK=True): + self.totalCol = len(columnHeaders) + self.columnHeaders = columnHeaders + self.resizeContentColumns = resizeContentColumns + self.requestedColumns = requestedColumns + self.needDataTypeChoice = needDataTypeChoice + self.colName = colName + self.tableName = tableName + self.checkFK = checkFK + super(Ui_DBInfoDialog, self).__init__() + + self.setGeometry(100, 100, 900, 900) + mainLayout = QtWidgets.QVBoxLayout() + self.setLayout(mainLayout) + + buttonLayout = self.createButtonsSection() + mainLayout.addLayout(buttonLayout) + + self.createDataTableWidget() + mainLayout.addWidget(self.dataTableWidget, 1) + if self.tableName != '': + instruction = ("Background: LIGHT BLUE - Non Editable due to " + "FK constrain; " + "WHITE - Editable. " + "Text: BLACK - Saved; RED - Not saved") + # TODO: add question mark button to give instruction + # if self.tableName == 'parameters': + # instruction += ( + # "\nValueColors is requested for multiColorDots plotType." + # "Value from low to high" + # "\nThe format for less than or equal value pair is: " + # "value:color" + # "\nThe format for greater than value pair is: " + # "+value:color with " + # "color=R,Y,G,M,C.\n Ex: 2.3:C|+4:M") + mainLayout.addWidget(QtWidgets.QLabel(instruction)) + + def addWidget(self, dataList, rowidx, colidx, foreignkey=False, + choices=None, range=None, fieldName=''): + if dataList is None: + text = str(rowidx) # row number + else: + text = (str(dataList[rowidx][colidx - 1]) + if rowidx < len(dataList) else '') + + if dataList is None: + widget = QtWidgets.QPushButton(text) + elif choices is None and range is None: + widget = QtWidgets.QLineEdit(text) + if fieldName == 'convertFactor': + # precision=6 + validator = QtGui.QDoubleValidator(0.0, 5.0, 6) + validator.setNotation(QtGui.QDoubleValidator.StandardNotation) + widget.setValidator(validator) + if text == '': + widget.setText('1') + elif choices: + widget = QtWidgets.QComboBox() + widget.addItems(choices) + widget.setCurrentText(text) + elif range: + widget = QtWidgets.QSpinBox() + widget.setMinimum(range[0]) + widget.setMaximum(range[1]) + if text in ["", None, 'None']: + widget.setValue(range[0]) + else: + widget.setValue(int(text)) + + if dataList is None: + setWidgetColor(widget) + widget.setFixedWidth(40) + widget.clicked.connect( + lambda: self.rowNumberClicked(widget)) + elif foreignkey: + setWidgetColor(widget, readOnly=True) + else: + new = False if rowidx < len(dataList) else True + setWidgetColor(widget, readOnly=False, changed=new) + if choices is None and range is None: + widget.textChanged.connect( + lambda changedtext: + self.cellInputChange(changedtext, rowidx, colidx)) + elif choices: + widget.currentTextChanged.connect( + lambda changedtext: + self.cellInputChange(changedtext, rowidx, colidx)) + elif range: + widget.valueChanged.connect( + lambda changedtext: + self.cellInputChange(changedtext, rowidx, colidx)) + self.dataTableWidget.setCellWidget(rowidx, colidx, widget) + + def rowNumberClicked(self, widget): + self.dataTableWidget.selectRow(int(widget.text())) + self.dataTableWidget.repaint() + + def createButtonsSection(self): + hLayout = QtWidgets.QHBoxLayout() + + if self.needDataTypeChoice: + self.dataTypeCombobox = QtWidgets.QComboBox(self) + dataTypeRows = executeDB('SELECT * FROM DataTypes') + self.dataTypeCombobox.addItems([d[0] for d in dataTypeRows]) + self.dataTypeCombobox.currentTextChanged.connect( + self.dataTypeChanged) + hLayout.addWidget(self.dataTypeCombobox) + if self.tableName != '': + self.addRowBtn = QtWidgets.QPushButton(self, text='ADD ROW') + self.addRowBtn.setFixedWidth(300) + self.addRowBtn.clicked.connect(self.addNewRow) + hLayout.addWidget(self.addRowBtn) + + self.saveChangesBtn = QtWidgets.QPushButton( + self, text='SAVE CHANGES') + self.saveChangesBtn.setFixedWidth(300) + self.saveChangesBtn.clicked.connect(self.saveChanges) + hLayout.addWidget(self.saveChangesBtn) + + self.closeBtn = QtWidgets.QPushButton(self, text='CLOSE') + self.closeBtn.setFixedWidth(300) + self.closeBtn.clicked.connect(self.close) + hLayout.addWidget(self.closeBtn) + return hLayout + + def createDataTableWidget(self): + self.dataTableWidget = QtWidgets.QTableWidget(self) + self.dataTableWidget.verticalHeader().hide() + self.dataTableWidget.setColumnCount(self.totalCol) + self.dataTableWidget.setHorizontalHeaderLabels(self.columnHeaders) + header = self.dataTableWidget.horizontalHeader() + header.setSectionResizeMode(QtWidgets.QHeaderView.Stretch) + for i in self.resizeContentColumns: + header.setSectionResizeMode( + i, QtWidgets.QHeaderView.ResizeToContents) + + if self.needDataTypeChoice: + self.dataType = self.dataTypeCombobox.currentText() + self.updateDataTableWidgetItems() + + def updateDataTableWidgetItems(self): + self.dataList = self.getDataList() + rowCount = len(self.dataList) + rowCount = 1 if rowCount == 0 else rowCount + self.dataTableWidget.setRowCount(rowCount) + for i in range(len(self.dataList)): + fk = self.checkDataForeignKey(self.dataList[i][0]) + self.addRow(i, fk) + if len(self.dataList) == 0: + """ + No Row, should leave 1 empty row + """ + self.clearFirstRow() + self.update() + + def addNewRow(self): + rowPosition = self.dataTableWidget.rowCount() + self.dataTableWidget.insertRow(rowPosition) + self.addRow(rowPosition) + self.dataTableWidget.scrollToBottom() + self.dataTableWidget.repaint() # to show row's header + self.dataTableWidget.cellWidget(rowPosition, 1).setFocus() + + def removeRow(self, removeRowidx): + self.dataTableWidget.removeRow(removeRowidx) + for i in range(removeRowidx, self.dataTableWidget.rowCount()): + cellWget = self.dataTableWidget.cellWidget(i, 0) + cellWget.setText(str(i)) + + def cellInputChange(self, changedText, rowidx, colidx): + """ + If cell's value is changed, text's color will be red + otherwise, text's color will be black + """ + changed = False + if rowidx < len(self.dataList): + if changedText != self.dataList[rowidx][colidx - 1]: + changed = True + cellWget = self.dataTableWidget.cellWidget(rowidx, colidx) + setWidgetColor(cellWget, changed=changed) + + def checkDataForeignKey(self, val): + if not self.checkFK: + return False + sql = (f"SELECT {self.colName} FROM channels " + f"WHERE {self.colName}='{val}'") + paramRows = executeDB(sql) + if len(paramRows) > 0: + return True + else: + return False + + def resetRowInputs(self, reset, widgetidx, listidx): + for colidx in range(1, self.totalCol): + cellWget = self.dataTableWidget.cellWidget(widgetidx, colidx) + readOnly = False + if hasattr(cellWget, 'isReadOnly'): + readOnly = cellWget.isReadOnly() + if not readOnly: + if reset == 1: + orgVal = self.dataList[listidx][colidx - 1] + if isinstance(cellWget, QtWidgets.QLineEdit): + cellWget.setText(str(orgVal)) + elif isinstance(cellWget, QtWidgets.QComboBox): + cellWget.setCurrentText(str(orgVal)) + elif isinstance(cellWget, QtWidgets.QSpinBox): + cellWget.setValue(int(orgVal)) + setWidgetColor(cellWget) + + def addRow(self, rowidx, fk=False): + pass + + def dataTypeChanged(self): + pass + + def saveChanges(self): + try: + self.dataTableWidget.focusWidget().clearFocus() + except AttributeError: + pass + self.removeCount = 0 + self.insertCount = 0 + self.skipCount = 0 + rowCount = self.dataTableWidget.rowCount() + for i in range(rowCount): + widgetidx = i - (rowCount - self.dataTableWidget.rowCount()) + listidx = i - self.removeCount + self.insertCount - self.skipCount + rowInputs = self.getRowInputs(widgetidx) + reset = self.updateData(rowInputs, widgetidx, listidx) + if reset > -1: + self.resetRowInputs(reset, widgetidx, listidx) + + def updateData(self, row, widgetidx, listidx, insertsql, updatesql): + """ + update dataTableWidget and dataList according to the action to + add, remove, update + :param row: values of processed row in dataTableWidget + :param widgetidx: index of the process rows in dataTableWidget + :param listidx: index of the process rows in self.dataList + :param colName: key db column in the table + :param tableName: data table name + :param insertsql: query to insert a row to the table + :param updatesql: query to update a row in the table + :return -1 for doing nothing + 0 no need to reset input values to org row + 1 need to reset input values to org row + """ + if listidx < len(self.dataList): + orgRow = self.dataList[listidx] + if row[0] == "": + msg = (f"The {self.colName} of '{orgRow}' at row {widgetidx} " + f"has been changed to blank. Are you sure you want to " + f"delete this row in database?") + result = QtWidgets.QMessageBox.question( + self, "Delete %s?" % orgRow, msg, + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No + ) + if result == QtWidgets.QMessageBox.No: + # reset the first widget value and call the function again + # to check other fields + cellWget = self.dataTableWidget.cellWidget(widgetidx, 1) + cellWget.setText(orgRow[0]) + row[0] = orgRow[0] + self.updateData(row, widgetidx, listidx) + else: + sql = (f"DELETE FROM {self.tableName} " + f"WHERE {self.colName}='{orgRow[0]}'") + executeDB(sql) + self.dataList.remove(orgRow) + self.removeRow(widgetidx) + self.removeCount += 1 + return -1 + + if row == orgRow: + return -1 + if (row[0] in [p[0] for p in self.dataList] and + self.dataList[listidx][0] != row[0]): + msg = (f"The {self.colName} of {orgRow} at row" + f" {widgetidx} has been changed to '{row[0]}' " + f"which already is in the database. " + f"It will be changed back to {orgRow[0]}.") + QtWidgets.QMessageBox.information(self, "Error", msg) + # reset the first widget value and call the function again + # to check other fields + cellWget = self.dataTableWidget.cellWidget(widgetidx, 1) + cellWget.setText(orgRow[0]) + row[0] = orgRow[0] + self.updateData(row, widgetidx, listidx) + else: + msg = (f"{orgRow} at row {widgetidx} has " + f"been changed to {row}. Please confirm it.") + result = QtWidgets.QMessageBox.question( + self, "Confirmation", msg, + QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel + ) + if result == QtWidgets.QMessageBox.Cancel: + return 1 + else: + executeDB(updatesql % orgRow[0]) + self.dataList[listidx] = row + return 0 + + if row[0] == "": + msg = (f"Row {widgetidx} has blank {self.colName}. " + f"It will be removed.") + QtWidgets.QMessageBox.information(self, "Error", msg) + self.removeRow(widgetidx) + return -1 + blankRequestedColumns = [self.requestedColumns[i] + for i in self.requestedColumns.keys() + if row[i-1] == ""] + if blankRequestedColumns != []: + msg = (f"Row {widgetidx}: blank " + f"{', '.join(blankRequestedColumns)} which require some " + f"value(s).") + QtWidgets.QMessageBox.information(self, "Error", msg) + return -1 + + if row[0] in [p[0] for p in self.dataList]: + msg = (f"The {self.colName} '{row[0]}' is already in the database." + f" Row {widgetidx} will be removed.") + QtWidgets.QMessageBox.information(self, "Error", msg) + self.removeRow(widgetidx) + return -1 + executeDB(insertsql) + self.dataList.append(row) + self.insertCount += 1 + return 0 diff --git a/sohstationviewer/view/filelist.py b/sohstationviewer/view/core/filelistwidget.py similarity index 100% rename from sohstationviewer/view/filelist.py rename to sohstationviewer/view/core/filelistwidget.py diff --git a/sohstationviewer/view/core/plottingWidget.py b/sohstationviewer/view/core/plottingWidget.py new file mode 100755 index 0000000000000000000000000000000000000000..b95de5c7f7cb9e6c3100b73af4f4f4dff5dcac0d --- /dev/null +++ b/sohstationviewer/view/core/plottingWidget.py @@ -0,0 +1,1029 @@ +import math + +from PySide2 import QtCore, QtWidgets +from matplotlib.backends.backend_qt5agg import ( + FigureCanvasQTAgg as Canvas) +from matplotlib import pyplot as pl +from matplotlib.patches import ConnectionPatch, Rectangle +from matplotlib.ticker import AutoMinorLocator +import numpy as np + +from sohstationviewer.controller.plottingData import ( + getTitle, getGaps, getTimeTicks, getUnitBitweight, getMassposValueColors) +from sohstationviewer.conf import constants +from sohstationviewer.conf.colorSettings import Clr, set_colors +from sohstationviewer.database import extractData +from sohstationviewer.controller.util import displayTrackingInfo, getVal + + +plotFunc = { + 'linesDots': ( + ("Lines, one color dots. "), + "plotLinesDots"), + 'linesSRate': ( + ("Lines, one color dots, bitweight info. "), + "plotLinesSRate"), + 'linesMasspos': ( + ("multi-line mass position, multi-color dots. "), + "plotLinesMasspos"), + # 'dotsMasspos': ( + # ("mass position, multi-color, single line. "), + # "plotDotsMasspos"), + 'dotForTime': ( + "Dots according to timestamp. Color defined by valueColors. Ex: G", + "plotTimeDots"), + 'multiColorDots': ( + ("Multicolor dots with colors defined by valueColors. " + "Value from low to high. " + "Ex:*:W or -1:_|0:R|2.3:Y|+2.3:G. With colors: RYGMC: _: not plot"), + "plotMultiColorDots" + ), + 'upDownDots': ( + ("Show data with 2 different values: first down/ second up. " + "With colors defined by valueColors. Ex: 1:R|0:Y"), + 'plotUpDownDots' + ) +} + + +class PlottingWidget(QtWidgets.QScrollArea): + """ + zorder: + axis spines: 0 + center line: 1 + lines: 2 + gap, dots, : 3 + """ + + def __init__(self, parent=None): + super().__init__() + self.parent = parent + self.plotNo = 0 + self.infoWidget = None + self.widgt = QtWidgets.QWidget(parent) + self.axes = [] + self.currplot_title = None + self.hidden_plots = {} + self.zoomMarker1Shown = False + self.axes = [] + + self.widthBase = 0.185 + # width of plotting area + self.plottingW = self.widthBase + # X1: 0.19: Width = 20% of 50in (Figure's width) + # X2: 0.19*2: Width = 40% of 50in + # X4: 0.19*4: Width = 80% of 50 in + # height of plotting area + # + changed when plots are added or removed + # + changed when changing the V-magnify param + self.plottingH = 0.996 + # this is the height of a standard plot + # plotH = 0.01 # 0.01: Height = 1% of 100in (Figure's height) + # left of plotting area: no change + self.plottingL = 0.03 + # bottom of plot gap, where we start to draw data + # self.plotGapB = 0.990 + # distance from left axis to start of label + self.labelPad = 100 + self.fontSize = 7 + """ + Set Figure size 50in width, 100in height. + This is the maximum size of plotting container. + add_axes will draw proportion to this size. + The actual view for user based on size of self.widgt. + """ + self.fig = pl.Figure(facecolor='white', figsize=(50, 100)) + self.fig.canvas.mpl_connect('pick_event', self.__on_pick_on_artist) + self.fig.canvas.mpl_connect('button_press_event', self.__on_pick) + + self.canvas = Canvas(self.fig) + self.canvas.setParent(self.widgt) + self.setWidget(self.widgt) + self.set_colors('B') + + def contextMenuEvent(self, event): + if self.axes == []: + return + contextMenu = QtWidgets.QMenu(self) + removePlotAct = contextMenu.addAction( + "Remove %s" % self.currplot_title) + removePlotAct.setStatusTip("Remove the current Plot") + removePlotAct.triggered.connect(self.hide_currplot) + + if self.hidden_plots != {}: + showAllPlotAct = contextMenu.addAction("Show All Plots") + showAllPlotAct.triggered.connect(self.show_all_hidden_plots) + showPlotActs = [] + for i in sorted(self.hidden_plots.keys()): + showPlotActs.append( + contextMenu.addAction('Show hidden Plot %s' % i)) + showPlotActs[-1].triggered.connect( + lambda *arg, index=i: self.show_hidden_plot(index)) + contextMenu.exec_(self.mapToGlobal(event.pos())) + + def __getTimestamp(self, event): + x, y = event.x, event.y # coordinate data + inv = self.axes[0].transData.inverted() + # convert to timestamp, bc event.xdata is None in the space bw axes + xdata = inv.transform((x, y))[0] + print('xdata of mouse: {:.2f}'.format(xdata)) + return xdata + + def __zoomBwMarkers(self, xdata): + if self.currMinX == xdata: + return + # self.fig.canvas.mpl_disconnect(self.follower) + self.__draw() + self.zoomMarker1Shown = False + [self.currMinX, self.currMaxX] = sorted( + [self.currMinX, xdata]) + print("ZM2 self.currMinX:", self.currMinX) + print("ZM2 self.currMaxX:", self.currMaxX) + self.__set_lim() + self.zoomMarker1.set_visible(False) + self.zoomMarker2.set_visible(False) + self.__draw() + + def __on_pick(self, event): + """ + xmouse, ymouse = event.mouseevent.xdata, event.mouseevent.ydata + # x, y = artist.get_xdata(), artist.get_ydata() + # ind = event.ind + print('Artist picked:', artist) + # print(self.plots) + # print('Index:', self.plots.index(event.artist)) + # print('{} vertices picked'.format(len(ind))) + # print('Pick between vertices {} and {}'.format( + min(ind), max(ind) + 1)) + print('x, y of mouse: {:.2f},{:.2f}'.format(xmouse, ymouse)) + # print('Data point:', x[ind[0]], y[ind[0]]) + print("mouseevent:", dir(event.mouseevent)) + print("guievent:", dir(event.guiEvent)) + print("modifiers:", event.guiEvent.modifiers()) + """ + # print(dir(event)) + modifiers = event.guiEvent.modifiers() + xdata = self.__getTimestamp(event) + if modifiers == QtCore.Qt.ShiftModifier: + print("shift+click") + if not self.zoomMarker1Shown: + self.ruler.set_visible(False) + self.__set_ruler_visibled(self.zoomMarker1, xdata) + self.currMinX = xdata + print("ZM1 self.currMinX:", self.currMinX) + self.zoomMarker1Shown = True + self.__set_ruler_visibled(self.zoomMarker2, xdata) + self.__draw() + else: + self.__zoomBwMarkers(xdata) + # if self.currMinX == xdata: + # return + # self.fig.canvas.mpl_disconnect(self.follower) + # self.__draw() + # self.zoomMarker1Shown = False + # [self.currMinX, self.currMaxX] = sorted( + # [self.currMinX, xdata]) + # print("ZM2 self.currMinX:", self.currMinX) + # print("ZM2 self.currMaxX:", self.currMaxX) + # self.__set_lim() + # self.zoomMarker1.set_visible(False) + # self.zoomMarker2.set_visible(False) + # self.__draw() + elif modifiers in [QtCore.Qt.ControlModifier, + QtCore.Qt.MetaModifier]: + print("Ctrl+click") + self.zoomMarker1.set_visible(False) + self.zoomMarker1Shown = False + self.fig.canvas.mpl_disconnect(self.follower) + self.__set_ruler_visibled(self.ruler, xdata) + else: + print("click xmouse:", xdata) + if self.zoomMarker1Shown: + self.__zoomBwMarkers(xdata) + + def __on_pick_on_artist(self, event): + print("============__on_pick ============") + """ + xmouse, ymouse = event.mouseevent.xdata, event.mouseevent.ydata + # x, y = artist.get_xdata(), artist.get_ydata() + # ind = event.ind + print('Artist picked:', artist) + # print(self.plots) + # print('Index:', self.plots.index(event.artist)) + # print('{} vertices picked'.format(len(ind))) + # print('Pick between vertices {} and {}'.format( + # min(ind), max(ind) + 1)) + print('x, y of mouse: {:.2f},{:.2f}'.format(xmouse, ymouse)) + # print('Data point:', x[ind[0]], y[ind[0]]) + print("mouseevent:", dir(event.mouseevent)) + print("guievent:", dir(event.guiEvent)) + print("modifiers:", event.guiEvent.modifiers()) + """ + # print(dir(event)) + artist = event.artist + # modifiers = event.guiEvent.modifiers() + print("artist:", artist) + if isinstance(artist, pl.Line2D): + x, y = artist.get_xdata(), artist.get_ydata() + ind = event.ind + print('Data point:', x[ind[0]], y[ind[0]]) + if isinstance(artist, pl.Axes): + self.currplot = artist + self.currplot_index = self.axes.index(artist) + self.currplot_title = "Plot %s" % self.currplot_index + + def __set_ruler_visibled(self, ruler, x): + ruler.set_visible(True) + ruler.xy1 = (x, 0) + ruler.xy2 = (x, self.bottom) + if ruler == self.zoomMarker2: + # make zoomMarker2 follow mouse. + # need to disconnect when state of rulers change + self.follower = self.fig.canvas.mpl_connect( + "motion_notify_event", self.__zoomMarker2_follow_mouse) + self.__draw() + + def __zoomMarker2_follow_mouse(self, mouseevent): + xdata = self.__getTimestamp(mouseevent) + self.zoomMarker2.xy1 = (xdata, 0) + self.zoomMarker2.xy2 = (xdata, self.bottom) + self.__draw() + + def keyPressEvent(self, event): + if event.key() == QtCore.Qt.Key_Escape: + print("ESC") + self.ruler.set_visible(False) + self.zoomMarker1.set_visible(False) + self.zoomMarker2.set_visible(False) + self.zoomMarker1Shown = False + self.__draw() + return super(PlottingWidget, self).keyPressEvent(event) + + def __add_timestamp_bar(self, usedHeight, top=True): + """ + set the axes to display timestampBar on top of the plotting area + setting axis off to not display at the begining + Color in qpeek is DClr[T0] + :return: + """ + self.plottingH -= usedHeight + timestampBar = self.canvas.figure.add_axes( + [self.plottingL, self.plottingH, self.plottingW, 0.00005], + ) + timestampBar.axis('off') + timestampBar.xaxis.set_minor_locator(AutoMinorLocator()) + timestampBar.spines['bottom'].set_color(self.displayColor['TM']) + timestampBar.spines['top'].set_color(self.displayColor['TM']) + + if top: + labelbottom = False + else: + labelbottom = True + timestampBar.tick_params(which='major', length=7, width=2, + direction='inout', + colors=self.displayColor['TM'], + labelbottom=labelbottom, + labeltop=not labelbottom) + timestampBar.tick_params(which='minor', length=4, width=1, + direction='inout', + colors=self.displayColor['TM']) + timestampBar.set_ylabel('Hours', + fontweight='bold', + fontsize=self.fontSize+2, + rotation=0, + labelpad=self.labelPad, + ha='left', + color=self.displayColor['TM']) + + # self.__update_timestamp_bar(timestampBar) + return timestampBar + + def __update_timestamp_bar(self, timestampBar): + times, majorTimes, majorTimeLabels = getTimeTicks( + self.currMinX, self.currMaxX, self.dateMode, self.timeTicksTotal) + timestampBar.axis('on') + timestampBar.set_yticks([]) + timestampBar.set_xticks(times, minor=True) + timestampBar.set_xticks(majorTimes) + timestampBar.set_xticklabels(majorTimeLabels, + fontsize=self.fontSize+2) + timestampBar.set_xlim(self.currMinX, self.currMaxX) + + def __create_axes(self, plotB, plotH, hasMinMaxLines=True): + ax = self.canvas.figure.add_axes( + [self.plottingL, plotB, self.plottingW, plotH], + picker=True + ) + + ax.spines['right'].set_visible(False) + ax.spines['left'].set_visible(False) + if hasMinMaxLines: + ax.spines['top'].set_zorder(0) + ax.spines['bottom'].set_zorder(0) + ax.spines['top'].set_color(self.displayColor['P0']) + ax.spines['bottom'].set_color(self.displayColor['P0']) + + ax.set_yticks([]) + ax.set_xticks([]) + ax.tick_params(colors=self.displayColor['TX'], + width=0, + pad=-2, + labelsize=self.fontSize) + ax.patch.set_alpha(0) + return ax + + def setAxesInfo(self, ax, sampleNoList, sampleNoClrList=None, + label=None, info='', y=None, chanDB=None, linkedAx=None): + if label is None: + label = chanDB['label'] + titleVerAlignment = 'center' + # set info undertitle + if linkedAx is not None: + info = label + if info != '': + ax.text( + -0.15, 0.2, + info, + horizontalalignment='left', + verticalalignment='top', + rotation='horizontal', + transform=ax.transAxes, + color=self.displayColor['TX'], + size=self.fontSize + ) + titleVerAlignment = 'top' + if linkedAx is None: + # set title on left side + ax.text( + -0.15, 0.6, + label, + horizontalalignment='left', + verticalalignment=titleVerAlignment, + rotation='horizontal', + transform=ax.transAxes, + color=self.displayColor['LB'], + size=self.fontSize + 2 + ) + + # set samples' total on right side + if sampleNoClrList is None: + sampleNoClrList = len(sampleNoList) * ['W'] + if len(sampleNoList) == 1: + ax.sampleLbl = ax.text( + 1.005, 0.5, + sampleNoList[0], + horizontalalignment='left', + verticalalignment='center', + rotation='horizontal', + transform=ax.transAxes, + color=Clr[sampleNoClrList[0]], + size=self.fontSize + ) + else: + # bottom + ax.sampleLbl = ax.text( + 1.005, 0.25, + sampleNoList[0], + horizontalalignment='left', + verticalalignment='center', + rotation='horizontal', + transform=ax.transAxes, + color=Clr[sampleNoClrList[0]], + size=self.fontSize + ) + # top + ax.sampleLbl = ax.text( + 1.005, 0.75, + sampleNoList[1], + horizontalalignment='left', + verticalalignment='center', + rotation='horizontal', + transform=ax.transAxes, + color=Clr[sampleNoClrList[1]], + size=self.fontSize + ) + + if y is None: + # draw center line + ax.plot([self.currMinX, self.currMaxX], + [0, 0], + color=self.displayColor['P0'], + linewidth=0.5, + zorder=1 + ) + ax.spines['top'].set_visible(False) + ax.spines['bottom'].set_visible(False) + else: + minY = min(y) + maxY = max(y) + ax.spines['top'].set_visible(True) + ax.spines['bottom'].set_visible(True) + ax.unit_bw = getUnitBitweight(chanDB, self.parent.bitweightOpt) + self.__setAxesYlim(ax, minY, maxY) + + def __setAxesYlim(self, ax, minY, maxY): + minY = round(minY, 7) + maxY = round(maxY, 7) + if maxY > minY: + ax.set_yticks([minY, maxY]) + ax.set_yticklabels( + [ax.unit_bw.format(minY), ax.unit_bw.format(maxY)]) + if minY == maxY: + maxY += 1 + ax.set_yticks([minY]) + ax.set_yticklabels([ax.unit_bw.format(minY)]) + ax.set_ylim(minY, maxY) + + def addGapBar(self, gaps): + """ + set the axes to display gapBar on top of the plotting area + setting axis off to not display at the begining + :return: + """ + if self.parent.minGap is None: + return + self.gaps = getGaps(gaps, float(self.parent.minGap)) + self.plottingH -= 0.003 + self.gapBar = self.__create_axes(self.plottingH, + 0.001, + hasMinMaxLines=False) + self.updateGapBar() + + def updateGapBar(self): + gapLabel = "%sm" % self.parent.minGap + # TODO: calculate gap limit + # self.gaps = [] + h = 0.001 # height of rectangle represent gap + self.setAxesInfo(self.gapBar, [len(self.gaps)], + label=gapLabel) + # draw gaps + for i in range(len(self.gaps)): + x = self.gaps[i][0] + w = self.gaps[i][1] - self.gaps[i][ + 0] # width of rectangle represent gap + self.gapBar.add_patch(Rectangle((x, - h / 2), w, h, + color='r', + picker=True, + lw=0., + zorder=3)) # on top of center line + + def __get_height(self, ratio): + plotH = 0.0012 * ratio # ratio with figure height + bwPlotsDistance = 0.0015 + self.plottingH -= plotH + bwPlotsDistance + self.plottingHPixel += 19 * ratio + return plotH + + # -------------------- Different color dots ----------------------- # + def plotNone(self): + """ + plot with nothing needed to show rulers + """ + plotH = 0.00001 + bwPlotsDistance = 0.0001 + self.plottingH -= plotH + bwPlotsDistance + ax = self.__create_axes(self.plottingH, plotH, hasMinMaxLines=False) + ax.x = None + ax.plot([0], [0], linestyle="") + return ax + + def plotMultiColorDots(self, cData, chanDB, chan, linkedAx): + """ + plot scattered dots with colors defined by valueColors: + *:W or -1:_|0:R|2.3:Y|+2.3:G + with colors: RYGMC in dbSettings.py + _: not plot + :param data: data of the channel which is list of (time, value) + :param chanDB: info of channel from DB + :return: + """ + + plotH = self.__get_height(chanDB['height']) + if linkedAx is None: + ax = self.__create_axes( + self.plottingH, plotH, hasMinMaxLines=False) + else: + ax = linkedAx + + x = [] + prevVal = -constants.HIGHEST_INT + + if chanDB['valueColors'] in [None, 'None', '']: + chanDB['valueColors'] = '*:W' + valueColors = chanDB['valueColors'].split('|') + for vc in valueColors: + v, c = vc.split(':') + val = getVal(v) + if c == '_': + prevVal = val + continue + + if v.startswith('+'): + points = [cData['decTimes'][i] + for i in range(len(cData['decData'])) + if cData['decData'][i] > val] + elif v == '*': + points = cData['decTimes'] + else: + points = [cData['decTimes'][i] + for i in range(len(cData['decData'])) + if prevVal < cData['decData'][i] <= val] + x += points + + ax.plot(points, len(points) * [0], linestyle="", + marker='s', markersize=0.5, zorder=3, + color=Clr[c], picker=True, pickradius=3) + prevVal = val + + totalSamples = len(x) + + x = sorted(x) + self.setAxesInfo(ax, [totalSamples], chanDB=chanDB, linkedAx=linkedAx) + if linkedAx is None: + ax.x = x + else: + ax.linkedX = x + return ax + + # def plotDotsMasspos(self, cData, chanDB, chan, linkedAx): + # valueColors = getMassposValueColors( + # self.parent.massPosVoltRangeOpt, chan, self.cMode, self.errors) + # if valueColors is None: + # return + # chanDB['valueColors'] = valueColors + # return self.plotMultiColorDots(cData, chanDB, chan, linkedAx) + + # ---------------------------- up/down dots ---------------------------- # + def plotUpDownDots(self, cData, chanDB, chan, linkedAx): + """ + data with 2 different values defined in valueColors + """ + plotH = self.__get_height(chanDB['height']) + if linkedAx is None: + ax = self.__create_axes( + self.plottingH, plotH, hasMinMaxLines=False) + else: + ax = linkedAx + + valCols = chanDB['valueColors'].split('|') + pointsList = [] + colors = [] + for vc in valCols: + v, c = vc.split(':') + val = getVal(v) + + points = [cData['decTimes'][i] + for i in range(len(cData['decData'])) + if cData['decData'][i] == val] + pointsList.append(points) + colors.append(c) + + # down dots + ax.plot(pointsList[0], len(pointsList[0]) * [-0.5], linestyle="", + marker='s', markersize=2, zorder=3, + color=Clr[colors[0]], picker=True, pickradius=3) + # up dots + ax.plot(pointsList[1], len(pointsList[1]) * [0.5], linestyle="", + marker='s', markersize=2, zorder=3, + color=Clr[colors[1]], picker=True, pickradius=3) + x = pointsList[0] + pointsList[1] + x = sorted(x) + ax.set_ylim(-2, 2) + self.setAxesInfo(ax, [len(pointsList[0]), len(pointsList[1])], + sampleNoClrList=colors, + chanDB=chanDB, + linkedAx=linkedAx) + if linkedAx is None: + ax.x = x + else: + ax.linkedX = x + return ax + + # ----------------------- dots for times, ignore data------------------- # + def plotTimeDots(self, cData, chanDB, chan, linkedAx): + plotH = self.__get_height(chanDB['height']) + if linkedAx is None: + ax = self.__create_axes(self.plottingH, plotH) + else: + ax = linkedAx + + color = 'W' + if chanDB['valueColors'] not in [None, 'None', '']: + color = chanDB['valueColors'].strip() + x = cData['decTimes'] + self.setAxesInfo(ax, [len(x)], chanDB=chanDB, linkedAx=linkedAx) + + ax.myPlot = ax.plot(x, [0]*len(x), marker='s', markersize=1.5, + linestyle='', zorder=2, + color=Clr[color], picker=True, + pickradius=3) + if linkedAx is None: + ax.x = x + else: + ax.linkedX = x + return ax + + # ----------------------- lines - one color dots ----------------------- # + def plotLinesDots(self, cData, chanDB, chan, linkedAx, info=''): + """ L:G|D:W """ + plotH = self.__get_height(chanDB['height']) + if linkedAx is None: + ax = self.__create_axes(self.plottingH, plotH) + else: + ax = linkedAx + + x, y = cData['decTimes'], cData['decData'] + self.setAxesInfo(ax, [len(x)], chanDB=chanDB, + info=info, y=y, linkedAx=linkedAx) + colors = {} + if chanDB['valueColors'] not in [None, 'None', '']: + colorParts = chanDB['valueColors'].split('|') + for cStr in colorParts: + obj, c = cStr.split(':') + colors[obj] = c + + lColor = 'G' + hasDot = False + if 'L' in colors: + lColor = colors['L'] + if 'D' in colors: + dColor = colors['D'] + hasDot = True + + if not hasDot: + ax.myPlot = ax.plot(x, y, + linestyle='-', linewidth=0.7, + color=Clr[lColor]) + else: + ax.myPlot = ax.plot(x, y, marker='s', markersize=1.5, + linestyle='-', linewidth=0.7, zorder=2, + color=Clr[lColor], + markerfacecolor=Clr[dColor], + picker=True, pickradius=3) + if linkedAx is None: + ax.x = x + ax.y = y + else: + ax.linkedX = x + ax.linkedY = y + return ax + + def plotLinesSRate(self, cData, chanDB, chan, linkedAx): + """ + multi-line line seismic, one color, line only, + can apply bit weights in (get_unit_bitweight()) + """ + if cData['samplerate'] >= 1.0: + info = "%dsps" % cData['samplerate'] + else: + info = "%gsps" % cData['samplerate'] + return self.plotLinesDots(cData, chanDB, chan, linkedAx, info=info) + + # ----------------------- lines - multi-color dots --------------------- # + def plotLinesMasspos(self, cData, chanDB, chan, linkedAx): + + # if (chan.startswith("MP") and + # chanDB['valueColors'] not in [None, 'None', '']): + # _colors = chanDB['valueColors'].split("|") + # _values = sorted(set(abs(cData['decData']))) + # + # valueColors = [(_values[idx], _colors[idx]) + # for idx in range(len(_values[:len(_colors)]))] + # else: + _values = sorted(set(abs(cData['decData']))) + print("_values:", _values) + valueColors = getMassposValueColors( + self.parent.massPosVoltRangeOpt, chan, + self.cMode, self.errors, retType='tupleList') + + if valueColors is None: + return + + plotH = self.__get_height(chanDB['height']) + ax = self.__create_axes(self.plottingH, plotH) + + ax.x, ax.y = cData['times'], cData['data'] + self.setAxesInfo(ax, [len(ax.x)], chanDB=chanDB, y=ax.y) + ax.myPlot = ax.plot(ax.x, ax.y, + linestyle='-', linewidth=0.7, + color=self.displayColor['PL'], + zorder=2)[0] + colors = [None] * len(ax.y) + sizes = [0.5] * len(ax.y) + for i in range(len(ax.y)): + count = 0 + prevV = 0 + for v, c in valueColors: + if count < (len(valueColors) - 1): + if prevV < abs(ax.y[i]) <= v: + colors[i] = Clr[c] + break + else: + # if abs(ax.y[i]) > v: + colors[i] = Clr[c] + break + prevV = v + count += 1 + ax.scatter(ax.x, ax.y, marker='s', c=colors, s=sizes, zorder=3) + return ax + + # ---------------------------------------------------------# + + def __add_ruler(self, color): + ruler = ConnectionPatch( + xyA=(0, 0), + xyB=(0, self.bottom), + coordsA="data", + coordsB="data", + axesA=self.timestampBarTop, + axesB=self.timestampBarBottom, + color=color, + ) + ruler.set_visible(False) + self.timestampBarBottom.add_artist(ruler) + return ruler + + def __set_lim(self, orgSize=False): + self.__update_timestamp_bar(self.timestampBarTop) + self.__update_timestamp_bar(self.timestampBarBottom) + if hasattr(self, 'gapBar'): + self.gapBar.set_xlim(self.currMinX, self.currMaxX) + if not orgSize: + newGaps = [g for g in self.gaps + if (self.currMinX <= g[0] <= self.currMaxX + or self.currMinX <= g[1] <= self.currMaxX)] + + # reset total of samples on the right + self.gapBar.sampleLbl.set_text(len(newGaps)) + for ax in self.axes: + ax.set_xlim(self.currMinX, self.currMaxX) + if ax.x is None: + # the plotNone bar is at the end, no need to process + break + if not orgSize: + # x, y + newX = [x for x in ax.x + if x >= self.currMinX and x <= self.currMaxX] + # reset total of samples on the right + ax.sampleLbl.set_text(len(newX)) + if len(newX) == 0: + continue + if hasattr(ax, 'y'): + # don't need to reset y range if ax.y not exist + newMinX = min(newX) + newMaxX = max(newX) + try: + newMinXIndex = ax.x.index(newMinX) + newMaxXIndex = ax.x.index(newMaxX) + except AttributeError: + newMinXIndex = np.where(ax.x == newMinX)[0][0] + newMaxXIndex = np.where(ax.x == newMaxX)[0][0] + newY = ax.y[newMinXIndex:newMaxXIndex + 1] + newMinY = min(newY) + newMaxY = max(newY) + self.__setAxesYlim(ax, newMinY, newMaxY) + + def __set_title(self, title): + self.fig.text(-0.15, 100, title, + verticalalignment='top', + horizontalalignment='left', + transform=self.timestampBarTop.transAxes, + color=self.displayColor['TX'], + size=self.fontSize) + + def __draw(self): + try: + self.canvas.draw() + # a bug on mac: + # not showing updated info until clicking on another window + # fix by calling repaint() + self.widgt.repaint() + except TypeError: + pass + + # ######## Functions for outside world ##### + def init_size(self): + geo = self.maximumViewportSize() + if self.plotNo == 0: + # set view size fit with the scroll's view port size + self.widgt.setFixedWidth(geo.width()) + self.widgt.setFixedHeight(geo.height()) + + def set_msg_widget(self, msgWidget): + self.msgWidget = msgWidget + + def set_background_color(self, color='black'): + self.fig.patch.set_facecolor(color) + self.__draw() + + def resetView(self): + """ + reset all zooms back to the first plotting + """ + if self.axes == []: + return + self.currMinX = self.minX + self.currMaxX = self.maxX + self.__set_lim() + self.__draw() + + def clear(self): + if self.zoomMarker1.get_visible(): + self.zoomMarker1.set_visible(False) + else: + self.zoomMarker1.set_visible(True) + # self.fig.clear() + # self.axes = [] + self.__draw() + + def decimateWConvertFactor(self, cData, convertFactor, maxDP=50000): + """ + convertFactor = 150mV/count = 150V/1000count + => unit data * convertFactor= data *150/1000 V + convert data in numpy arrays to a reduced list + according to max number of datapoint needed + """ + cData['data'] = np.multiply(cData['data'], [convertFactor]) + ln = cData['times'].size + d = math.ceil(ln / maxDP) # decimation factor + + if d < 5: + cData['decTimes'] = cData['times'] # np array + cData['decData'] = cData['data'] + else: + cData['decTimes'] = [cData['times'][i] + for i in range(ln) if i % d == 0] + cData['decData'] = [cData['data'][i] + for i in range(ln) if i % d == 0] + return cData + + def addPlots(self, setID, plottingData, reqInfoChans, timeTicksTotal): + """ + :param setID: (netID, statID, locID) + :param plottingData: a ditionary including: + { gaps: [(t1,t2),(t1,t2),...] (in epoch time) + channels:{cha: {netID, statID, locID, chanID, times, data, + samplerate, startTmEpoch, endTmEpoch} # + earliestUTC: the earliest time of all channels + latestUTC: the latest time of all channels + :timeTicksTotal: max number of tick to show on time bar + Data set: {channelname: [(x,y), (x,y)...] + """ + # print('plottingData:', plottingData) + self.processingLog = [] # [(message, type)] + self.plottingData = plottingData + self.errors = [] + if self.axes != []: + self.fig.clear() + self.dateMode = self.parent.dateFormat.upper() + self.timeTicksTotal = timeTicksTotal + self.minX = self.currMinX = plottingData['earliestUTC'] + self.maxX = self.currMaxX = plottingData['latestUTC'] + self.plotNo = len(plottingData['channels']) + title = getTitle(self, setID, plottingData, self.dateMode) + + self.plottingHPixel = 200 + # self.plottingH = self.plotGapB + self.axes = [] + + self.timestampBarTop = self.__add_timestamp_bar(0.003) + self.__set_title(title) + self.addGapBar(plottingData['gaps']) + notFoundChan = [c for c in reqInfoChans + if c not in plottingData['channels'].keys()] + if len(notFoundChan) > 0: + msg = (f"The following channels is in Channel Preferences but " + f"not in the given data: {notFoundChan}") + print(msg) + self.processingLog.append((msg, 'warning')) + print("all channels:", plottingData['channels'].keys()) + for chan in plottingData['channels'].keys(): + print(">>>>CHAN:", chan) + chanDB = extractData.getChanPlotInfo(chan, self.parent.dataType) + print("chanDB:", chanDB) + if chanDB['height'] == 0: + # not draw + continue + if chanDB['channel'] == 'DEFAULT': + msg = (f"Channel {chan}'s definition can't be found database.") + displayTrackingInfo(self.parent, msg, 'warning') + + plotType = chanDB['plotType'] + if chanDB['plotType'] == '': + continue + + cData = self.decimateWConvertFactor( + plottingData['channels'][chan], + chanDB['convertFactor'], + 50000) + linkedAx = None + if chanDB['linkedChan'] not in [None, 'None', '']: + try: + linkedAx = plottingData['channels'][ + chanDB['linkedChan']]['ax'] + except KeyError: + pass + ax = getattr(self, + plotFunc[plotType][1])(cData, chanDB, chan, linkedAx) + if ax is None: + continue + plottingData['channels'][chan]['ax'] = ax + ax.chan = chan + if linkedAx is None: + self.axes.append(ax) + + self.axes.append(self.plotNone()) + self.timestampBarBottom = self.__add_timestamp_bar(0.003, top=False) + self.__set_lim(orgSize=True) + self.bottom = self.axes[-1].get_ybound()[0] + self.ruler = self.__add_ruler(self.displayColor['TR']) + self.zoomMarker1 = self.__add_ruler(self.displayColor['ZM']) + self.zoomMarker2 = self.__add_ruler(self.displayColor['ZM']) + # Set view size fit with the given data + if self.widgt.geometry().height() < self.plottingHPixel: + self.widgt.setFixedHeight(self.plottingHPixel) + + self.__draw() + + def hide_plots(self, plot_indexes): + if self.axes == []: + return + plot_indexes = sorted(plot_indexes) + idx = 0 + total_h = 0 + for i in range(plot_indexes[0], len(self.axes)): + pos = self.axes[i].get_position() + pos.y0 += total_h + pos.y1 += total_h + if idx < len(plot_indexes) and i == plot_indexes[idx]: + h = pos.y1 - pos.y0 + total_h += h + pos.y0 = pos.y1 + idx += 1 + self.hidden_plots[i] = h + self.axes[i].set_position(pos) + + # currently consider every plot height are all 100px height + height = self.widgt.geometry().height() - 100 * len(plot_indexes) + self.widgt.setFixedHeight(height) + self.__draw() + + def hide_currplot(self): + pos = self.currplot.get_position() + h = pos.y1 - pos.y0 + pos.y0 = pos.y1 + self.currplot.set_position(pos) + for i in range(self.currplot_index + 1, len(self.axes)): + pos = self.axes[i].get_position() + pos.y0 += h + pos.y1 += h + self.axes[i].set_position(pos) + # currently consider every plot height are all 100px height + height = self.widgt.geometry().height() - 100 + self.widgt.setFixedHeight(height) + self.hidden_plots[self.currplot_index] = h + self.__draw() + + def show_hidden_plot(self, index): + h = self.hidden_plots[index] + workplot = self.axes[index] + pos = workplot.get_position() + pos.y1 = pos.y0 + h + workplot.set_position(pos) + for i in range(index, len(self.axes)): + pos = self.axes[i].get_position() + pos.y0 -= h + pos.y1 -= h + self.axes[i].set_position(pos) + # currently consider every plot height are all 100px height + height = self.widgt.geometry().height() + 100 + self.widgt.setFixedHeight(height) + del self.hidden_plots[index] + pos = workplot.get_position() + self.__draw() + + def show_all_hidden_plots(self): + plot_indexes = sorted(self.hidden_plots.keys()) + idx = 0 + total_h = 0 + for i in range(plot_indexes[0], len(self.axes)): + pos = self.axes[i].get_position() + if idx < len(plot_indexes) and i == plot_indexes[idx]: + h = self.hidden_plots[i] + total_h += h + pos.y1 = pos.y0 + h + idx += 1 + del self.hidden_plots[i] + pos.y0 -= total_h + pos.y1 -= total_h + self.axes[i].set_position(pos) + + # currently consider every plot height are all 100px height + height = self.widgt.geometry().height() + 100 * len(plot_indexes) + self.widgt.setFixedHeight(height) + self.__draw() + + def set_colors(self, mode): + self.cMode = mode + self.displayColor = set_colors(mode) + self.fig.patch.set_facecolor(self.displayColor['MF']) diff --git a/sohstationviewer/view/dataTypedialog.py b/sohstationviewer/view/dataTypedialog.py new file mode 100755 index 0000000000000000000000000000000000000000..a546a2ddfb15fa2c90a001cbe229bb4e8d118d4a --- /dev/null +++ b/sohstationviewer/view/dataTypedialog.py @@ -0,0 +1,34 @@ +""" +datatypedialog.py +GUI to add/edit/remove dataTypes +NOTE: Cannot remove or change dataTypes that already have channels. +""" + +from sohstationviewer.view.core.dbgui_superclass import Ui_DBInfoDialog +from sohstationviewer.database.proccessDB import executeDB + + +class DataTypeDialog(Ui_DBInfoDialog): + def __init__(self, parent): + super().__init__(parent, ['No.', 'DataType'], 'dataType', 'dataTypes', + resizeContentColumns=[0]) + self.setWindowTitle("Edit/Add/Delete DataTypes") + + def addRow(self, rowidx, fk=False): + self.addWidget(None, rowidx, 0) # No. + self.addWidget(self.dataList, rowidx, 1, foreignkey=fk) + + def getDataList(self): + dataTypeRows = executeDB('SELECT * FROM DataTypes') + return [[d[0]] for d in dataTypeRows] + + def getRowInputs(self, rowidx): + return [self.dataTableWidget.cellWidget(rowidx, 1).text().strip()] + + def updateData(self, row, widgetidx, listidx): + insertsql = (f"INSERT INTO DataTypes VALUES('{row[0]}')") + updatesql = (f"UPDATE DataTypes SET dataType='{row[0]}' " + f"WHERE dataType='%s'") + + return super().updateData( + row, widgetidx, listidx, insertsql, updatesql) diff --git a/sohstationviewer/view/mainwindow.py b/sohstationviewer/view/mainwindow.py index 30b7c785670952fdd90c528ad15770ed6b72cd45..b7a90bbec32f2ba16f2ac2ae8cb1561edcf28b4b 100755 --- a/sohstationviewer/view/mainwindow.py +++ b/sohstationviewer/view/mainwindow.py @@ -1,21 +1,21 @@ import pathlib +import os -from PySide2 import QtCore, QtGui, QtWidgets +from PySide2 import QtCore, QtWidgets from sohstationviewer.view.ui.main_ui import Ui_MainWindow from sohstationviewer.view.calendardialog import CalendarDialog -from sohstationviewer.view.calendarwidget import CalendarWidget -from sohstationviewer.view.filelist import FileListItem +from sohstationviewer.view.core.filelistwidget import FileListItem +from sohstationviewer.controller.processing import loadData, detectDataType +from sohstationviewer.view.dataTypedialog import DataTypeDialog +from sohstationviewer.view.paramdialog import ParamDialog +from sohstationviewer.view.channeldialog import ChannelDialog +from sohstationviewer.view.plottypedialog import PlotTypeDialog +from sohstationviewer.view.channelpreferdialog import ChannelPreferDialog +from sohstationviewer.database.proccessDB import executeDB_dict class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): - """ - Implements the core logic for the Ui_MainWindow class - produced by Qt Designer. Any custom slots / signals - should be implemented in this class. Any modifications to - Ui_MainWindow *will* be lost if the UI is ever updated in - Qt Designer. - """ currentDirectoryChanged = QtCore.Signal(str) @@ -23,128 +23,78 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): super().__init__(parent) self.setupUi(self) - cwd = str(pathlib.Path().resolve().parent) - self.setCurrentDirectory(cwd) - - # File menu - self.deleteSetup.triggered.connect(self.deleteSetupFile) - - # Commands menu - self.openGPSPlots.triggered.connect(self.openGpsPlot) - self.searchLog.triggered.connect(self.openLogSearch) - self.plotTimeRanges.triggered.connect(self.openPlotTimeRanges) - self.plotPositions.triggered.connect(self.openPlotPositions) - - # Commands/Export submenu - self.exportDeploymentFile.triggered.connect(self.exportDepLine) - self.exportDeploymentBlock.triggered.connect(self.exportDepBlock) - self.exportTSPGeometry.triggered.connect(self.exportTspGeom) - self.exportShotInfo.triggered.connect(self.exportShotInf) - - # Help menu - self.openCalendar.triggered.connect(self.openCalendarWidget) - - # Options Menu - self.sortGroup = QtWidgets.QActionGroup(self) - self.sortGroup.addAction(self.sortFilesByType) - self.sortGroup.addAction(self.sortFilesAlphabetically) - self.sortFilesByType.setChecked(True) - - self.colorGroup = QtWidgets.QActionGroup(self) - self.colorGroup.addAction(self.colorMPRegular) - self.colorGroup.addAction(self.colorMPTrillium) - self.colorMPRegular.setChecked(True) - - self.dateGroup = QtWidgets.QActionGroup(self) - self.dateGroup.addAction(self.showYYYYDOYDates) - self.dateGroup.addAction(self.showYYYY_MM_DDDates) - self.dateGroup.addAction(self.showYYYYMMMDDDates) - # self.showYYYYDOYDates.setChecked(True) - self.showYYYY_MM_DDDates.setChecked(True) - - # Connect slots to change the date format displayed - # by the QDateEdit widgets - # self.showYYYYDOYDates.triggered.connect( - # lambda: self.setDateFormat('yyyy:D')) - self.showYYYY_MM_DDDates.triggered.connect( - lambda: self.setDateFormat('yyyy-MM-dd')) - self.showYYYYMMMDDDates.triggered.connect( - lambda: self.setDateFormat('yyyyMMMdd')) - - self.timeToDateEdit.setCalendarWidget(CalendarWidget(self)) - self.timeToDateEdit.setDate(QtCore.QDate.currentDate()) - - self.timeFromDateEdit.setCalendarWidget(CalendarWidget(self)) - self.timeFromDateEdit.setDate(QtCore.QDate.currentDate()) - - # self.showYYYYDOYDates.triggered.emit() - self.showYYYY_MM_DDDates.triggered.emit() - - self.openFilesList.itemDoubleClicked.connect( - self.openFilesListItemDoubleClicked) - - pal = self.openFilesList.palette() - pal.setColor(QtGui.QPalette.Highlight, QtGui.QColor(128, 255, 128)) - pal.setColor(QtGui.QPalette.HighlightedText, QtGui.QColor(0, 0, 0)) - self.openFilesList.setPalette(pal) + # Options + self.dateFormat = 'YYYY-MM-DD' + self.massPosVoltRangeOpt = 'regular' + self.bitweightOpt = '' + self.getChannelPrefer() + self.YYYY_MM_DDAction.triggered.emit() - @QtCore.Slot() - def deleteSetupFile(self): - print('Deleting setup file.') - - @QtCore.Slot() - def openGpsPlot(self): - print('Opening GPS plot.') - - @QtCore.Slot() - def openLogSearch(self): - print('Opening Log search.') + def resizeEvent(self, event): + self.plottingWidget.init_size() @QtCore.Slot() - def openPlotTimeRanges(self): - print('Opening Time Ranges Plot.') + def openDataType(self): + win = DataTypeDialog(self) + win.show() @QtCore.Slot() - def openPlotPositions(self): - print('Opening Positions Plot.') + def openParam(self): + win = ParamDialog(self) + win.show() @QtCore.Slot() - def exportDepLine(self): - print('Exporting Deployment File (Line).') + def openChannel(self): + win = ChannelDialog(self) + win.show() @QtCore.Slot() - def exportDepBlock(self): - print('Exporting Deployment File (Block).') + def openCalendarWidget(self): + calendar = CalendarDialog(self) + calendar.show() @QtCore.Slot() - def exportTspGeom(self): - print('Exporting TSP Shot File / Geometry.') + def openPlotType(self): + win = PlotTypeDialog(self) + win.show() @QtCore.Slot() - def exportShotInf(self): - print('Exporting Shot Info.') + def openChannelPreferences(self): + dirnames = [os.path.join(self.cwdLineEdit.text(), item.text()) + for item in self.openFilesList.selectedItems()] + if dirnames == []: + msg = "No directories has been selected." + QtWidgets.QMessageBox.warning(self, "Select directory", msg) + return + win = ChannelPreferDialog(self, dirnames) + win.show() @QtCore.Slot() - def readSelectedFile(self): - print('Reading currently selected file.') - - # TODO: Launch a separate thread, so that - # the UI doesn't hang while data is being read. - # Otherwise, the user won't be able to cancel. + def allChanClicked(self): + if not self.allChanCheckBox.isChecked(): + if self.IDs == []: + self.allChanCheckBox.setChecked(True) + else: + self.currIDsNameLineEdit.setText(self.IDsName) + else: + self.currIDsNameLineEdit.setText('') @QtCore.Slot() - def stopFileRead(self): - print('Abandoning file read.') + def replotLoadedData(self): + self.plottingWidget.resetView() @QtCore.Slot() - def writePSFile(self): - print('Writing PS file.') + def setDateFormat(self, displayFormat): + """ + Sets the calendar format used by the QDateEdit text boxes. + :param displayFormat: str + A valid display format to be used for date conversion. + """ + self.timeToDateEdit.setDisplayFormat(displayFormat) + self.timeFromDateEdit.setDisplayFormat(displayFormat) + self.dateFormat = displayFormat @QtCore.Slot() - def reloadFile(self): - print('Reloading last file.') - - @QtCore.Slot(FileListItem) def openFilesListItemDoubleClicked(self, item): """ Handles the double-click event emitted when a user double-clicks on an @@ -161,31 +111,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): # i.e., path.open(), or path.iterdir() ... # path = item.filePath - @QtCore.Slot(str) - def setDateFormat(self, displayFormat): - """ - Sets the calendar format used by the QDateEdit - text boxes. - - Parameters - ---------- - displayFormat : str - A valid display format to be used for date conversion. - """ - self.timeToDateEdit.setDisplayFormat(displayFormat) - self.timeFromDateEdit.setDisplayFormat(displayFormat) - - def setCurrentDirectory(self, path=''): - # Remove entries when cwd changes - self.openFilesList.clear() - # Signal cwd changed, and gather list of files in new cwd - self.currentDirectoryChanged.emit(path) - for dent in pathlib.Path(path).iterdir(): - if not dent.is_dir() or dent.name.startswith('.'): - continue - - self.openFilesList.addItem(FileListItem(dent)) - @QtCore.Slot() def changeCurrentDirectory(self): """ @@ -201,11 +126,95 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): self.setCurrentDirectory(new_path) @QtCore.Slot() - def openCalendarWidget(self): + def readSelectedFiles(self): + print('Reading currently selected file.') + + # TODO: Launch a separate thread, so that + # the UI doesn't hang while data is being read. + # Otherwise, the user won't be able to cancel. + + # reqInfoChans = [ + # 'ACE', 'LOG', 'HH1', 'HH2', 'HHZ', 'LCE', 'LCQ', + # 'LH1', 'LH2', 'LHZ', 'VCO', 'VEA', 'VEC', 'VEP', + # 'VKI', 'VM1', 'VM2', 'VM3', 'VPB'] + reqInfoChans = (self.IDs if not self.allChanCheckBox.isChecked() + else []) + + # TODO: Having a form for user to create the list of channels to draw """ - Constructs a subclass of QCalendarDialog - which implements additional functionality to display - Julian dates alongside standard calendar format. + reqInfoChans: list of chans to read data from + It can be all chans in db or preference list of chans + For Reftek, the list of channels is fixed => may not need """ - calendar = CalendarDialog(self) - calendar.show() + dirnames = [os.path.join(self.cwdLineEdit.text(), item.text()) + for item in self.openFilesList.selectedItems()] + if dirnames == []: + msg = "No directories has been selected." + QtWidgets.QMessageBox.warning(self, "Select directory", msg) + return + self.dataType = detectDataType(self, dirnames) + if self.dataType is None: + return + + reqDSs = [] + for idx, DSCheckbox in enumerate(self.dsCheckBoxes): + if DSCheckbox.isChecked(): + reqDSs.append(idx + 1) + + if (not self.allChanCheckBox.isChecked() and + self.dataType != self.IDsDataType): + msg = (f"DataType detected for the selected data set is " + f"{self.dataType} which is different to IDs' " + f"{self.IDsDataType}.\n" + f"SOHStationViewer will read all data available.\n" + f"Do you want to continue?") + result = QtWidgets.QMessageBox.question( + self, "Confirmation", msg, + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) + if result == QtWidgets.QMessageBox.No: + return + self.allChanCheckBox.setChecked(True) + self.currIDsNameLineEdit.setText('') + reqInfoChans = [] + + plottingDataSets = loadData( + self.dataType, self, dirnames, reqInfoChans, reqDSs) + if self.detectGapCheckBox.isChecked(): + self.minGap = self.gapLenLineEdit.text() + else: + self.minGap = None + if len(plottingDataSets) == 1: + setID = list(plottingDataSets.keys())[0] + plottingData = plottingDataSets[setID] + else: + # TODO: create form with buttons of all sets for user to choose + # which one to plot + print("ask user with set of net,stat,loc they want to look at") + timeTickTotal = 5 # TODO: let user choose max ticks to be displayed + reqInfoChans = (reqInfoChans if reqInfoChans != [] + else plottingData['channels'].keys()) + print("MAin Window: all channels:", plottingData['channels'].keys()) + self.plottingWidget.addPlots(setID, plottingData, + reqInfoChans, timeTickTotal) + + def setCurrentDirectory(self, path=''): + # Remove entries when cwd changes + self.openFilesList.clear() + # Signal cwd changed, and gather list of files in new cwd + self.currentDirectoryChanged.emit(path) + for dent in pathlib.Path(path).iterdir(): + if not dent.is_dir() or dent.name.startswith('.'): + continue + + self.openFilesList.addItem(FileListItem(dent)) + + def getChannelPrefer(self): + self.IDsName = '' + self.IDs = [] + self.dataType = 'Unknown' + rows = executeDB_dict('SELECT name, IDs, dataType FROM ChannelPrefer ' + 'WHERE current=1') + if len(rows) > 0: + self.IDsName = rows[0]['name'] + self.IDs = [t.strip() for t in rows[0]['IDs'].split(',')] + self.IDsDataType = rows[0]['dataType'] diff --git a/sohstationviewer/view/paramdialog.py b/sohstationviewer/view/paramdialog.py new file mode 100755 index 0000000000000000000000000000000000000000..9cdfcc2cf91ba7fa14f4c852194d23d23d6db72c --- /dev/null +++ b/sohstationviewer/view/paramdialog.py @@ -0,0 +1,65 @@ +""" +paramdialog.py +GUI to add/dit/remove params +NOTE: Cannot remove or change params that are already used for channels. +""" +from PySide2 import QtWidgets + +from sohstationviewer.view.core.dbgui_superclass import Ui_DBInfoDialog +from sohstationviewer.view.core import plottingWidget +from sohstationviewer.database.proccessDB import executeDB +from sohstationviewer.conf.dbSettings import conf + + +class ParamDialog(Ui_DBInfoDialog): + def __init__(self, parent): + super().__init__( + parent, + ['No.', 'Param', 'Plot Type', 'ValueColors', 'Height '], + 'param', 'parameters', + resizeContentColumns=[0, 3]) + self.setWindowTitle("Edit/Add/Delete Parameters") + + def addRow(self, rowidx, fk=False): + self.addWidget(None, rowidx, 0) # No. + self.addWidget(self.dataList, rowidx, 1, foreignkey=fk) + self.addWidget(self.dataList, rowidx, 2, + choices=[''] + sorted(plottingWidget.plotFunc.keys())) + self.addWidget(self.dataList, rowidx, 3) + self.addWidget(self.dataList, rowidx, 4, + range=[0, 10]) + + def getDataList(self): + paramRows = executeDB('SELECT * FROM Parameters') + return [[d[0], + '' if d[1] is None else d[1], + d[2]] + for d in paramRows] + + def getRowInputs(self, rowidx): + # check vallueColors string + valueColorsString = self.dataTableWidget.cellWidget( + rowidx, 3).currentText().strip() + valueColors = valueColorsString.split("|") + for vc in valueColors: + if not conf['valColRE'].match(vc): + msg = (f"The valueColor is requested for '{vc}' at line " + f"{rowidx}does not match the required format:" + f"[+]value:color with color=R,Y,G,M,C." + f"\n Ex: 2.3:C|+4:M") + QtWidgets.QMessageBox.information(self, "Error", msg) + return [ + self.dataTableWidget.cellWidget(rowidx, 1).text().strip(), + self.dataTableWidget.cellWidget(rowidx, 2).currentText().strip(), + self.dataTableWidget.cellWidget(rowidx, 3).currentText().strip(), + int(self.dataTableWidget.cellWidget(rowidx, 4).text()) + ] + + def updateData(self, row, widgetidx, listidx): + insertsql = (f"INSERT INTO Parameters VALUES" + f"('{row[0]}', '{row[1]}', {row[2]})") + updatesql = (f"UPDATE Parameters SET param='{row[0]}', " + f"plotType='{row[1]}', height={row[2]} " + f"WHERE param='%s'") + return super().updateData( + row, widgetidx, listidx, insertsql, updatesql) diff --git a/sohstationviewer/view/plottypedialog.py b/sohstationviewer/view/plottypedialog.py new file mode 100755 index 0000000000000000000000000000000000000000..4611d3eef0d7e87c0050a61f33f7dfecafd84709 --- /dev/null +++ b/sohstationviewer/view/plottypedialog.py @@ -0,0 +1,24 @@ +""" +plottypedialog +GUI to view the types of plotting and their descriptions +NOTE: plottypes are defined in plottingWidget +""" + +from sohstationviewer.view.core.dbgui_superclass import Ui_DBInfoDialog +from sohstationviewer.view.core import plottingWidget + + +class PlotTypeDialog(Ui_DBInfoDialog): + def __init__(self, parent): + super().__init__( + parent, ['No.', ' Plot Type ', 'Description'], + '', '', resizeContentColumns=[0, 1], checkFK=False) + self.setWindowTitle("Plotting Types") + + def addRow(self, rowidx, fk=False): + self.addWidget(None, rowidx, 0) # No. + self.addWidget(self.dataList, rowidx, 1, foreignkey=True) + self.addWidget(self.dataList, rowidx, 2, foreignkey=True) + + def getDataList(self): + return [[key, val[0]] for key, val in plottingWidget.plotFunc.items()] diff --git a/sohstationviewer/view/ui/about_ui.py b/sohstationviewer/view/ui/about_ui_qtdesigner.py similarity index 100% rename from sohstationviewer/view/ui/about_ui.py rename to sohstationviewer/view/ui/about_ui_qtdesigner.py diff --git a/sohstationviewer/view/ui/calendar_ui.py b/sohstationviewer/view/ui/calendar_ui_qtdesigner.py similarity index 78% rename from sohstationviewer/view/ui/calendar_ui.py rename to sohstationviewer/view/ui/calendar_ui_qtdesigner.py index 0b272eefdeecaf28f1fc43c7fe325d647f9c0d41..34e8bfe7329fc58cfc6afa4bd9c9ef2b93d007ff 100644 --- a/sohstationviewer/view/ui/calendar_ui.py +++ b/sohstationviewer/view/ui/calendar_ui_qtdesigner.py @@ -8,7 +8,10 @@ # # WARNING! All changes made in this file will be lost! -from PySide2 import QtCore, QtGui, QtWidgets +from PySide2 import QtCore, QtWidgets + +from sohstationviewer.view.core.calendarwidget import CalendarWidget + class Ui_CalendarDialog(object): def setupUi(self, CalendarDialog): @@ -29,11 +32,12 @@ class Ui_CalendarDialog(object): self.verticalLayout.addLayout(self.horizontalLayout) self.retranslateUi(CalendarDialog) - QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL("accepted()"), CalendarDialog.accept) - QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL("rejected()"), CalendarDialog.reject) + QtCore.QObject.connect( + self.buttonBox, QtCore.SIGNAL("accepted()"), CalendarDialog.accept) + QtCore.QObject.connect( + self.buttonBox, QtCore.SIGNAL("rejected()"), CalendarDialog.reject) QtCore.QMetaObject.connectSlotsByName(CalendarDialog) def retranslateUi(self, CalendarDialog): - CalendarDialog.setWindowTitle(QtWidgets.QApplication.translate("CalendarDialog", "Calendar", None, -1)) - -from sohstationviewer.view.calendarwidget import CalendarWidget + CalendarDialog.setWindowTitle(QtWidgets.QApplication.translate( + "CalendarDialog", "Calendar", None, -1)) diff --git a/sohstationviewer/view/ui/main_ui.py b/sohstationviewer/view/ui/main_ui.py old mode 100644 new mode 100755 index 53dac5eabc5eea9e30d13a17f9870570d5a11d90..aecc4a4d09575a99021c93b4aa6b7bf7a4e98b8c --- a/sohstationviewer/view/ui/main_ui.py +++ b/sohstationviewer/view/ui/main_ui.py @@ -1,635 +1,458 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'sohstationviewer/view/ui/main.ui', -# licensing of 'sohstationviewer/view/ui/main.ui' applies. -# -# Created: Wed Jun 2 10:54:48 2021 -# by: pyside2-uic running on PySide2 5.13.2 -# -# WARNING! All changes made in this file will be lost! +# UI and connectSignals for MainWindow from PySide2 import QtCore, QtGui, QtWidgets +from sohstationviewer.view.core.calendarwidget import CalendarWidget +from sohstationviewer.view.core.plottingWidget import PlottingWidget + + class Ui_MainWindow(object): def setupUi(self, MainWindow): - MainWindow.setObjectName("MainWindow") + self.MainWindow = MainWindow MainWindow.resize(1798, 1110) - MainWindow.setUnifiedTitleAndToolBarOnMac(False) - self.centralwidget = QtWidgets.QWidget(MainWindow) - self.centralwidget.setObjectName("centralwidget") - self.gridLayout = QtWidgets.QGridLayout(self.centralwidget) - self.gridLayout.setContentsMargins(2, 2, 2, 2) - self.gridLayout.setObjectName("gridLayout") - self.cwdPushButton = QtWidgets.QPushButton(self.centralwidget) - self.cwdPushButton.setObjectName("cwdPushButton") - self.gridLayout.addWidget(self.cwdPushButton, 0, 0, 1, 1) - self.cwdLineEdit = QtWidgets.QLineEdit(self.centralwidget) - self.cwdLineEdit.setObjectName("cwdLineEdit") - self.gridLayout.addWidget(self.cwdLineEdit, 0, 1, 1, 1) - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) - self.gridLayout.addItem(spacerItem, 0, 2, 1, 1) - self.timeFromGrid = QtWidgets.QGridLayout() - self.timeFromGrid.setObjectName("timeFromGrid") - self.timeFromLabel = QtWidgets.QLabel(self.centralwidget) - self.timeFromLabel.setObjectName("timeFromLabel") - self.timeFromGrid.addWidget(self.timeFromLabel, 0, 0, 1, 1) - self.timeFromDateEdit = QtWidgets.QDateEdit(self.centralwidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.timeFromDateEdit.sizePolicy().hasHeightForWidth()) - self.timeFromDateEdit.setSizePolicy(sizePolicy) - self.timeFromDateEdit.setObjectName("timeFromDateEdit") + MainWindow.setWindowTitle("SOH Station Viewer") + self.centralWidget = QtWidgets.QWidget(MainWindow) + MainWindow.setCentralWidget(self.centralWidget) + + mainLayout = QtWidgets.QVBoxLayout() + mainLayout.setContentsMargins(5, 5, 5, 5) + mainLayout.setSpacing(0) + self.centralWidget.setLayout(mainLayout) + self.setFirstRow(mainLayout) + self.setSecondRow(mainLayout) + self.trackingInfoTextBrowser = QtWidgets.QTextBrowser( + self.centralWidget) + self.trackingInfoTextBrowser.setFixedHeight(60) + mainLayout.addWidget(self.trackingInfoTextBrowser) + self.createMenuBar(MainWindow) + self.connectSignals(MainWindow) + + def setFirstRow(self, mainLayout): + hLayout = QtWidgets.QHBoxLayout() + hLayout.setContentsMargins(2, 2, 2, 2) + hLayout.setSpacing(8) + mainLayout.addLayout(hLayout) + + self.cwdButton = QtWidgets.QPushButton( + "Main Data Directory", self.centralWidget) + hLayout.addWidget(self.cwdButton) + + self.cwdLineEdit = QtWidgets.QLineEdit( + self.centralWidget) + hLayout.addWidget(self.cwdLineEdit, 1) + + hLayout.addSpacing(40) + + hLayout.addWidget(QtWidgets.QLabel('From')) + self.timeFromDateEdit = QtWidgets.QDateEdit( + self.centralWidget) self.timeFromDateEdit.setCalendarPopup(True) self.timeFromDateEdit.setDisplayFormat("yyyy-MM-dd") - self.timeFromGrid.addWidget(self.timeFromDateEdit, 0, 1, 1, 1) - self.gridLayout.addLayout(self.timeFromGrid, 0, 3, 1, 1) - self.timeToGrid = QtWidgets.QGridLayout() - self.timeToGrid.setObjectName("timeToGrid") - self.timeToDateEdit = QtWidgets.QDateEdit(self.centralwidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.timeToDateEdit.sizePolicy().hasHeightForWidth()) - self.timeToDateEdit.setSizePolicy(sizePolicy) - self.timeToDateEdit.setObjectName("timeToDateEdit") + hLayout.addWidget(self.timeFromDateEdit) + + hLayout.addWidget(QtWidgets.QLabel('To')) + self.timeToDateEdit = QtWidgets.QDateEdit( + self.centralWidget) self.timeToDateEdit.setCalendarPopup(True) self.timeToDateEdit.setDisplayFormat("yyyy-MM-dd") - self.timeToGrid.addWidget(self.timeToDateEdit, 0, 1, 1, 1) - self.timeToLabel = QtWidgets.QLabel(self.centralwidget) - self.timeToLabel.setObjectName("timeToLabel") - self.timeToGrid.addWidget(self.timeToLabel, 0, 0, 1, 1) - self.gridLayout.addLayout(self.timeToGrid, 0, 4, 1, 1) - self.verticalSplit = QtWidgets.QSplitter(self.centralwidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.verticalSplit.sizePolicy().hasHeightForWidth()) - self.verticalSplit.setSizePolicy(sizePolicy) - self.verticalSplit.setOrientation(QtCore.Qt.Vertical) - self.verticalSplit.setHandleWidth(2) - self.verticalSplit.setChildrenCollapsible(False) - self.verticalSplit.setObjectName("verticalSplit") - self.mainWidget = QtWidgets.QWidget(self.verticalSplit) - self.mainWidget.setObjectName("mainWidget") - self.gridLayout_3 = QtWidgets.QGridLayout(self.mainWidget) - self.gridLayout_3.setContentsMargins(0, 0, 0, 0) - self.gridLayout_3.setObjectName("gridLayout_3") - self.horizontalSplit = QtWidgets.QSplitter(self.mainWidget) - self.horizontalSplit.setOrientation(QtCore.Qt.Horizontal) - self.horizontalSplit.setHandleWidth(2) - self.horizontalSplit.setChildrenCollapsible(False) - self.horizontalSplit.setObjectName("horizontalSplit") - self.sideScrollArea = QtWidgets.QScrollArea(self.horizontalSplit) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.sideScrollArea.sizePolicy().hasHeightForWidth()) - self.sideScrollArea.setSizePolicy(sizePolicy) - self.sideScrollArea.setMinimumSize(QtCore.QSize(0, 0)) - self.sideScrollArea.setMaximumSize(QtCore.QSize(250, 16777215)) - self.sideScrollArea.setWidgetResizable(True) - self.sideScrollArea.setObjectName("sideScrollArea") - self.sideScrollAreaContents = QtWidgets.QWidget() - self.sideScrollAreaContents.setGeometry(QtCore.QRect(0, 0, 264, 815)) - self.sideScrollAreaContents.setObjectName("sideScrollAreaContents") - self.verticalLayout = QtWidgets.QVBoxLayout(self.sideScrollAreaContents) - self.verticalLayout.setSpacing(2) - self.verticalLayout.setContentsMargins(0, 0, 0, 0) - self.verticalLayout.setObjectName("verticalLayout") - self.primaryGrid = QtWidgets.QGridLayout() - self.primaryGrid.setContentsMargins(0, 0, 0, 0) - self.primaryGrid.setObjectName("primaryGrid") - self.sep0 = QtWidgets.QFrame(self.sideScrollAreaContents) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.sep0.sizePolicy().hasHeightForWidth()) - self.sep0.setSizePolicy(sizePolicy) - self.sep0.setFrameShape(QtWidgets.QFrame.HLine) - self.sep0.setFrameShadow(QtWidgets.QFrame.Sunken) - self.sep0.setObjectName("sep0") - self.primaryGrid.addWidget(self.sep0, 12, 1, 1, 1) - self.dsGrid = QtWidgets.QGridLayout() - self.dsGrid.setObjectName("dsGrid") - self.ds6CheckBox = QtWidgets.QCheckBox(self.sideScrollAreaContents) - self.ds6CheckBox.setObjectName("ds6CheckBox") - self.dsGrid.addWidget(self.ds6CheckBox, 2, 3, 1, 1) - self.ds3CheckBox = QtWidgets.QCheckBox(self.sideScrollAreaContents) - self.ds3CheckBox.setObjectName("ds3CheckBox") - self.dsGrid.addWidget(self.ds3CheckBox, 0, 3, 1, 1) - self.dsTPSCheckBox = QtWidgets.QCheckBox(self.sideScrollAreaContents) - self.dsTPSCheckBox.setObjectName("dsTPSCheckBox") - self.dsGrid.addWidget(self.dsTPSCheckBox, 3, 2, 1, 1) - self.dsLabel = QtWidgets.QLabel(self.sideScrollAreaContents) - self.dsLabel.setObjectName("dsLabel") - self.dsGrid.addWidget(self.dsLabel, 2, 0, 1, 1) - spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.dsGrid.addItem(spacerItem1, 2, 4, 1, 1) - self.ds2CheckBox = QtWidgets.QCheckBox(self.sideScrollAreaContents) - self.ds2CheckBox.setObjectName("ds2CheckBox") - self.dsGrid.addWidget(self.ds2CheckBox, 0, 2, 1, 1) - self.ds1CheckBox = QtWidgets.QCheckBox(self.sideScrollAreaContents) - self.ds1CheckBox.setObjectName("ds1CheckBox") - self.dsGrid.addWidget(self.ds1CheckBox, 0, 1, 1, 1) - self.ds4CheckBox = QtWidgets.QCheckBox(self.sideScrollAreaContents) - self.ds4CheckBox.setObjectName("ds4CheckBox") - self.dsGrid.addWidget(self.ds4CheckBox, 2, 1, 1, 1) - self.ds5checkBox = QtWidgets.QCheckBox(self.sideScrollAreaContents) - self.ds5checkBox.setObjectName("ds5checkBox") - self.dsGrid.addWidget(self.ds5checkBox, 2, 2, 1, 1) - self.primaryGrid.addLayout(self.dsGrid, 11, 1, 1, 1) - self.sep2 = QtWidgets.QFrame(self.sideScrollAreaContents) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.sep2.sizePolicy().hasHeightForWidth()) - self.sep2.setSizePolicy(sizePolicy) - self.sep2.setFrameShape(QtWidgets.QFrame.HLine) - self.sep2.setFrameShadow(QtWidgets.QFrame.Sunken) - self.sep2.setObjectName("sep2") - self.primaryGrid.addWidget(self.sep2, 7, 1, 1, 1) - self.massPosGrid = QtWidgets.QGridLayout() - self.massPosGrid.setObjectName("massPosGrid") - self.massPosLabel = QtWidgets.QLabel(self.sideScrollAreaContents) - self.massPosLabel.setObjectName("massPosLabel") - self.massPosGrid.addWidget(self.massPosLabel, 0, 0, 1, 1) - self.hiChanCheckbox = QtWidgets.QCheckBox(self.sideScrollAreaContents) - self.hiChanCheckbox.setObjectName("hiChanCheckbox") - self.massPosGrid.addWidget(self.hiChanCheckbox, 0, 2, 1, 1) - self.lowChanCheckBox = QtWidgets.QCheckBox(self.sideScrollAreaContents) - self.lowChanCheckBox.setObjectName("lowChanCheckBox") - self.massPosGrid.addWidget(self.lowChanCheckBox, 0, 1, 1, 1) - spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.massPosGrid.addItem(spacerItem2, 0, 3, 1, 1) - self.primaryGrid.addLayout(self.massPosGrid, 9, 1, 1, 1) - self.controlButtonGrid = QtWidgets.QGridLayout() - self.controlButtonGrid.setSpacing(2) - self.controlButtonGrid.setObjectName("controlButtonGrid") - self.writePushButton = QtWidgets.QPushButton(self.sideScrollAreaContents) - self.writePushButton.setObjectName("writePushButton") - self.controlButtonGrid.addWidget(self.writePushButton, 0, 3, 1, 1) - self.stopPushButton = QtWidgets.QPushButton(self.sideScrollAreaContents) - self.stopPushButton.setObjectName("stopPushButton") - self.controlButtonGrid.addWidget(self.stopPushButton, 0, 2, 1, 1) - self.readPushButton = QtWidgets.QPushButton(self.sideScrollAreaContents) - self.readPushButton.setObjectName("readPushButton") - self.controlButtonGrid.addWidget(self.readPushButton, 0, 1, 1, 1) - spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.controlButtonGrid.addItem(spacerItem3, 0, 0, 1, 1) - spacerItem4 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.controlButtonGrid.addItem(spacerItem4, 0, 4, 1, 1) - self.primaryGrid.addLayout(self.controlButtonGrid, 13, 1, 1, 1) - self.sep1 = QtWidgets.QFrame(self.sideScrollAreaContents) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.sep1.sizePolicy().hasHeightForWidth()) - self.sep1.setSizePolicy(sizePolicy) - self.sep1.setFrameShape(QtWidgets.QFrame.HLine) - self.sep1.setFrameShadow(QtWidgets.QFrame.Sunken) - self.sep1.setObjectName("sep1") - self.primaryGrid.addWidget(self.sep1, 10, 1, 1, 1) - self.sohCheckBox = QtWidgets.QCheckBox(self.sideScrollAreaContents) - self.sohCheckBox.setObjectName("sohCheckBox") - self.primaryGrid.addWidget(self.sohCheckBox, 8, 1, 1, 1) - self.listWidget_2 = QtWidgets.QListWidget(self.sideScrollAreaContents) - self.listWidget_2.setObjectName("listWidget_2") - self.primaryGrid.addWidget(self.listWidget_2, 15, 0, 1, 3) - self.mainOptionsGrid = QtWidgets.QGridLayout() - self.mainOptionsGrid.setHorizontalSpacing(1) - self.mainOptionsGrid.setVerticalSpacing(2) - self.mainOptionsGrid.setObjectName("mainOptionsGrid") - self.searchLineEdit = QtWidgets.QLineEdit(self.sideScrollAreaContents) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.searchLineEdit.sizePolicy().hasHeightForWidth()) - self.searchLineEdit.setSizePolicy(sizePolicy) - self.searchLineEdit.setMinimumSize(QtCore.QSize(100, 0)) - self.searchLineEdit.setMaximumSize(QtCore.QSize(150, 16777215)) - self.searchLineEdit.setObjectName("searchLineEdit") - self.mainOptionsGrid.addWidget(self.searchLineEdit, 2, 0, 1, 1) - self.fileListGrid = QtWidgets.QGridLayout() - self.fileListGrid.setSpacing(2) - self.fileListGrid.setObjectName("fileListGrid") - self.fileListLogCheckBox = QtWidgets.QCheckBox(self.sideScrollAreaContents) - self.fileListLogCheckBox.setObjectName("fileListLogCheckBox") - self.fileListGrid.addWidget(self.fileListLogCheckBox, 0, 1, 1, 1) - self.fileListZipCheckBox = QtWidgets.QCheckBox(self.sideScrollAreaContents) - self.fileListZipCheckBox.setObjectName("fileListZipCheckBox") - self.fileListGrid.addWidget(self.fileListZipCheckBox, 1, 2, 1, 1) - self.fileListCfCheckBox = QtWidgets.QCheckBox(self.sideScrollAreaContents) - self.fileListCfCheckBox.setObjectName("fileListCfCheckBox") - self.fileListGrid.addWidget(self.fileListCfCheckBox, 1, 1, 1, 1) - self.fileListRefCheckBox = QtWidgets.QCheckBox(self.sideScrollAreaContents) - self.fileListRefCheckBox.setObjectName("fileListRefCheckBox") - self.fileListGrid.addWidget(self.fileListRefCheckBox, 0, 2, 1, 1) - self.label_4 = QtWidgets.QLabel(self.sideScrollAreaContents) - self.label_4.setObjectName("label_4") - self.fileListGrid.addWidget(self.label_4, 0, 0, 1, 1) - spacerItem5 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.fileListGrid.addItem(spacerItem5, 0, 5, 1, 1) - self.mainOptionsGrid.addLayout(self.fileListGrid, 3, 0, 1, 1) - self.backgroundGrid = QtWidgets.QGridLayout() - self.backgroundGrid.setSpacing(2) - self.backgroundGrid.setObjectName("backgroundGrid") - self.backgroundWhiteRadioButton = QtWidgets.QRadioButton(self.sideScrollAreaContents) - self.backgroundWhiteRadioButton.setChecked(True) - self.backgroundWhiteRadioButton.setObjectName("backgroundWhiteRadioButton") - self.buttonGroup = QtWidgets.QButtonGroup(MainWindow) - self.buttonGroup.setObjectName("buttonGroup") - self.buttonGroup.addButton(self.backgroundWhiteRadioButton) - self.backgroundGrid.addWidget(self.backgroundWhiteRadioButton, 0, 1, 1, 1) - self.backgroundBlackRadioButton = QtWidgets.QRadioButton(self.sideScrollAreaContents) - self.backgroundBlackRadioButton.setObjectName("backgroundBlackRadioButton") - self.buttonGroup.addButton(self.backgroundBlackRadioButton) - self.backgroundGrid.addWidget(self.backgroundBlackRadioButton, 0, 2, 1, 1) - self.backgroundLabel = QtWidgets.QLabel(self.sideScrollAreaContents) - self.backgroundLabel.setObjectName("backgroundLabel") - self.backgroundGrid.addWidget(self.backgroundLabel, 0, 0, 1, 1) - spacerItem6 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.backgroundGrid.addItem(spacerItem6, 0, 3, 1, 1) - self.mainOptionsGrid.addLayout(self.backgroundGrid, 4, 0, 1, 1) - self.plotControlGrid = QtWidgets.QGridLayout() - self.plotControlGrid.setSpacing(2) - self.plotControlGrid.setObjectName("plotControlGrid") - self.clearPushButton = QtWidgets.QPushButton(self.sideScrollAreaContents) - self.clearPushButton.setObjectName("clearPushButton") - self.plotControlGrid.addWidget(self.clearPushButton, 0, 0, 1, 1) - self.replotPushButton = QtWidgets.QPushButton(self.sideScrollAreaContents) - self.replotPushButton.setObjectName("replotPushButton") - self.plotControlGrid.addWidget(self.replotPushButton, 1, 0, 1, 1) - self.reloadPushButton = QtWidgets.QPushButton(self.sideScrollAreaContents) - self.reloadPushButton.setObjectName("reloadPushButton") - self.plotControlGrid.addWidget(self.reloadPushButton, 2, 0, 1, 1) - self.mainOptionsGrid.addLayout(self.plotControlGrid, 2, 1, 2, 1) - self.primaryGrid.addLayout(self.mainOptionsGrid, 4, 0, 1, 2) - self.openFilesList = QtWidgets.QListWidget(self.sideScrollAreaContents) - self.openFilesList.setObjectName("openFilesList") - self.primaryGrid.addWidget(self.openFilesList, 2, 0, 1, 2) - self.verticalLayout.addLayout(self.primaryGrid) - self.sideScrollArea.setWidget(self.sideScrollAreaContents) - self.mainScrollArea = QtWidgets.QScrollArea(self.horizontalSplit) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.mainScrollArea.sizePolicy().hasHeightForWidth()) - self.mainScrollArea.setSizePolicy(sizePolicy) - self.mainScrollArea.setWidgetResizable(True) - self.mainScrollArea.setObjectName("mainScrollArea") - self.mainScrollAreaContents = QtWidgets.QWidget() - self.mainScrollAreaContents.setGeometry(QtCore.QRect(0, 0, 1540, 829)) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(5) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.mainScrollAreaContents.sizePolicy().hasHeightForWidth()) - self.mainScrollAreaContents.setSizePolicy(sizePolicy) - self.mainScrollAreaContents.setObjectName("mainScrollAreaContents") - self.gridLayout_2 = QtWidgets.QGridLayout(self.mainScrollAreaContents) - self.gridLayout_2.setSpacing(2) - self.gridLayout_2.setContentsMargins(0, 0, 0, 0) - self.gridLayout_2.setObjectName("gridLayout_2") - # self.plottingWidget = PlottingWidget(self.mainScrollAreaContents) - # self.plottingWidget.setObjectName("plottingWidget") - # self.gridLayout_2.addWidget(self.plottingWidget, 0, 0, 1, 1) - self.mainScrollArea.setWidget(self.mainScrollAreaContents) - self.gridLayout_3.addWidget(self.horizontalSplit, 0, 0, 1, 1) - self.trackingInfoTextBrowser = QtWidgets.QTextBrowser(self.verticalSplit) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Maximum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.trackingInfoTextBrowser.sizePolicy().hasHeightForWidth()) - self.trackingInfoTextBrowser.setSizePolicy(sizePolicy) - self.trackingInfoTextBrowser.setMaximumSize(QtCore.QSize(16777215, 200)) - self.trackingInfoTextBrowser.setObjectName("trackingInfoTextBrowser") - self.gridLayout.addWidget(self.verticalSplit, 1, 0, 1, 5) - MainWindow.setCentralWidget(self.centralwidget) - self.menubar = QtWidgets.QMenuBar(MainWindow) - self.menubar.setGeometry(QtCore.QRect(0, 0, 1798, 20)) - self.menubar.setObjectName("menubar") - self.menuFile = QtWidgets.QMenu(self.menubar) - self.menuFile.setObjectName("menuFile") - self.menuCommand = QtWidgets.QMenu(self.menubar) - self.menuCommand.setObjectName("menuCommand") - self.menuExport_TT_Times_As = QtWidgets.QMenu(self.menuCommand) - self.menuExport_TT_Times_As.setObjectName("menuExport_TT_Times_As") - self.menuOptions = QtWidgets.QMenu(self.menubar) - self.menuOptions.setObjectName("menuOptions") - self.menuHelp = QtWidgets.QMenu(self.menubar) - self.menuHelp.setObjectName("menuHelp") - self.menuForm = QtWidgets.QMenu(self.menubar) - self.menuForm.setObjectName("menuForm") - self.menuPlots = QtWidgets.QMenu(self.menubar) - self.menuPlots.setObjectName("menuPlots") - MainWindow.setMenuBar(self.menubar) - self.statusbar = QtWidgets.QStatusBar(MainWindow) - self.statusbar.setObjectName("statusbar") - MainWindow.setStatusBar(self.statusbar) - self.quit = QtWidgets.QAction(MainWindow) - self.quit.setObjectName("quit") - self.deleteSetup = QtWidgets.QAction(MainWindow) - self.deleteSetup.setObjectName("deleteSetup") - self.openGPSPlots = QtWidgets.QAction(MainWindow) - self.openGPSPlots.setObjectName("openGPSPlots") - self.searchLog = QtWidgets.QAction(MainWindow) - self.searchLog.setObjectName("searchLog") - self.plotTimeRanges = QtWidgets.QAction(MainWindow) - self.plotTimeRanges.setObjectName("plotTimeRanges") - self.plotPositions = QtWidgets.QAction(MainWindow) - self.plotPositions.setObjectName("plotPositions") - self.exportDeploymentFile = QtWidgets.QAction(MainWindow) - self.exportDeploymentFile.setObjectName("exportDeploymentFile") - self.exportDeploymentBlock = QtWidgets.QAction(MainWindow) - self.exportDeploymentBlock.setObjectName("exportDeploymentBlock") - self.exportTSPGeometry = QtWidgets.QAction(MainWindow) - self.exportTSPGeometry.setObjectName("exportTSPGeometry") - self.exportShotInfo = QtWidgets.QAction(MainWindow) - self.exportShotInfo.setObjectName("exportShotInfo") - self.openCalendar = QtWidgets.QAction(MainWindow) - self.openCalendar.setObjectName("openCalendar") - self.plotDSPClkDifference = QtWidgets.QAction(MainWindow) - self.plotDSPClkDifference.setCheckable(True) - self.plotDSPClkDifference.setObjectName("plotDSPClkDifference") - self.plotPhaseError = QtWidgets.QAction(MainWindow) - self.plotPhaseError.setCheckable(True) - self.plotPhaseError.setObjectName("plotPhaseError") - self.plotJerk = QtWidgets.QAction(MainWindow) - self.plotJerk.setCheckable(True) - self.plotJerk.setObjectName("plotJerk") - self.plotFileErrors = QtWidgets.QAction(MainWindow) - self.plotFileErrors.setCheckable(True) - self.plotFileErrors.setObjectName("plotFileErrors") - self.plotGPSOnOffErr = QtWidgets.QAction(MainWindow) - self.plotGPSOnOffErr.setCheckable(True) - self.plotGPSOnOffErr.setObjectName("plotGPSOnOffErr") - self.plotGPSLkUnlk = QtWidgets.QAction(MainWindow) - self.plotGPSLkUnlk.setCheckable(True) - self.plotGPSLkUnlk.setObjectName("plotGPSLkUnlk") - self.plotTemperature = QtWidgets.QAction(MainWindow) - self.plotTemperature.setCheckable(True) - self.plotTemperature.setObjectName("plotTemperature") - self.plotVoltage = QtWidgets.QAction(MainWindow) - self.plotVoltage.setCheckable(True) - self.plotVoltage.setObjectName("plotVoltage") - self.plotBackupVoltage = QtWidgets.QAction(MainWindow) - self.plotBackupVoltage.setCheckable(True) - self.plotBackupVoltage.setObjectName("plotBackupVoltage") - self.plotDumpCall = QtWidgets.QAction(MainWindow) - self.plotDumpCall.setCheckable(True) - self.plotDumpCall.setObjectName("plotDumpCall") - self.plotAcquisitionOnOff = QtWidgets.QAction(MainWindow) - self.plotAcquisitionOnOff.setCheckable(True) - self.plotAcquisitionOnOff.setObjectName("plotAcquisitionOnOff") - self.plotResetPowerUp = QtWidgets.QAction(MainWindow) - self.plotResetPowerUp.setCheckable(True) - self.plotResetPowerUp.setObjectName("plotResetPowerUp") - self.plotErrorWarning = QtWidgets.QAction(MainWindow) - self.plotErrorWarning.setCheckable(True) - self.plotErrorWarning.setObjectName("plotErrorWarning") - self.plotDescrepancies = QtWidgets.QAction(MainWindow) - self.plotDescrepancies.setCheckable(True) - self.plotDescrepancies.setObjectName("plotDescrepancies") - self.plotSOHDataDefinitions = QtWidgets.QAction(MainWindow) - self.plotSOHDataDefinitions.setCheckable(True) - self.plotSOHDataDefinitions.setObjectName("plotSOHDataDefinitions") - self.plotNetworkUpDown = QtWidgets.QAction(MainWindow) - self.plotNetworkUpDown.setCheckable(True) - self.plotNetworkUpDown.setObjectName("plotNetworkUpDown") - self.plotEvents = QtWidgets.QAction(MainWindow) - self.plotEvents.setCheckable(True) - self.plotEvents.setObjectName("plotEvents") - self.plotDisk1Usage = QtWidgets.QAction(MainWindow) - self.plotDisk1Usage.setCheckable(True) - self.plotDisk1Usage.setObjectName("plotDisk1Usage") - self.plotDisk2Usage = QtWidgets.QAction(MainWindow) - self.plotDisk2Usage.setCheckable(True) - self.plotDisk2Usage.setObjectName("plotDisk2Usage") - self.plotMassPositions123 = QtWidgets.QAction(MainWindow) - self.plotMassPositions123.setCheckable(True) - self.plotMassPositions123.setObjectName("plotMassPositions123") - self.plotMassPositions456 = QtWidgets.QAction(MainWindow) - self.plotMassPositions456.setCheckable(True) - self.plotMassPositions456.setObjectName("plotMassPositions456") - self.plotAll = QtWidgets.QAction(MainWindow) - self.plotAll.setObjectName("plotAll") - self.plotTimingProblems = QtWidgets.QAction(MainWindow) - self.plotTimingProblems.setCheckable(True) - self.plotTimingProblems.setObjectName("plotTimingProblems") - self.filterNonSOHLines = QtWidgets.QAction(MainWindow) - self.filterNonSOHLines.setCheckable(True) - self.filterNonSOHLines.setObjectName("filterNonSOHLines") - self.sortFilesByType = QtWidgets.QAction(MainWindow) - self.sortFilesByType.setCheckable(True) - self.sortFilesByType.setObjectName("sortFilesByType") - self.sortFilesAlphabetically = QtWidgets.QAction(MainWindow) - self.sortFilesAlphabetically.setCheckable(True) - self.sortFilesAlphabetically.setObjectName("sortFilesAlphabetically") - self.calculateFileSizes = QtWidgets.QAction(MainWindow) - self.calculateFileSizes.setCheckable(True) - self.calculateFileSizes.setObjectName("calculateFileSizes") - self.warnIfBig = QtWidgets.QAction(MainWindow) - self.warnIfBig.setCheckable(True) - self.warnIfBig.setObjectName("warnIfBig") - self.addMassPositionToSOH = QtWidgets.QAction(MainWindow) - self.addMassPositionToSOH.setCheckable(True) - self.addMassPositionToSOH.setObjectName("addMassPositionToSOH") - self.colorMPRegular = QtWidgets.QAction(MainWindow) - self.colorMPRegular.setCheckable(True) - self.colorMPRegular.setObjectName("colorMPRegular") - self.colorMPTrillium = QtWidgets.QAction(MainWindow) - self.colorMPTrillium.setCheckable(True) - self.colorMPTrillium.setObjectName("colorMPTrillium") - self.addPositionsToET = QtWidgets.QAction(MainWindow) - self.addPositionsToET.setCheckable(True) - self.addPositionsToET.setObjectName("addPositionsToET") - self.readAntellopeLog = QtWidgets.QAction(MainWindow) - self.readAntellopeLog.setCheckable(True) - self.readAntellopeLog.setObjectName("readAntellopeLog") - self.showYYYYDOYDates = QtWidgets.QAction(MainWindow) - self.showYYYYDOYDates.setCheckable(True) - self.showYYYYDOYDates.setObjectName("showYYYYDOYDates") - self.showYYYY_MM_DDDates = QtWidgets.QAction(MainWindow) - self.showYYYY_MM_DDDates.setCheckable(True) - self.showYYYY_MM_DDDates.setObjectName("showYYYY_MM_DDDates") - self.showYYYYMMMDDDates = QtWidgets.QAction(MainWindow) - self.showYYYYMMMDDDates.setCheckable(True) - self.showYYYYMMMDDDates.setObjectName("showYYYYMMMDDDates") - self.setFontSizes = QtWidgets.QAction(MainWindow) - self.setFontSizes.setObjectName("setFontSizes") - self.openAbout = QtWidgets.QAction(MainWindow) - self.openAbout.setObjectName("openAbout") - self.menuFile.addAction(self.deleteSetup) - self.menuFile.addSeparator() - self.menuFile.addAction(self.quit) - self.menuExport_TT_Times_As.addAction(self.exportDeploymentFile) - self.menuExport_TT_Times_As.addAction(self.exportDeploymentBlock) - self.menuExport_TT_Times_As.addAction(self.exportTSPGeometry) - self.menuExport_TT_Times_As.addAction(self.exportShotInfo) - self.menuCommand.addAction(self.openGPSPlots) - self.menuCommand.addAction(self.searchLog) - self.menuCommand.addAction(self.plotTimeRanges) - self.menuCommand.addAction(self.plotPositions) - self.menuCommand.addSeparator() - self.menuCommand.addAction(self.menuExport_TT_Times_As.menuAction()) - self.menuOptions.addAction(self.plotTimingProblems) - self.menuOptions.addAction(self.filterNonSOHLines) - self.menuOptions.addSeparator() - self.menuOptions.addAction(self.sortFilesByType) - self.menuOptions.addAction(self.sortFilesAlphabetically) - self.menuOptions.addAction(self.calculateFileSizes) - self.menuOptions.addAction(self.warnIfBig) - self.menuOptions.addSeparator() - self.menuOptions.addAction(self.addMassPositionToSOH) - self.menuOptions.addAction(self.colorMPRegular) - self.menuOptions.addAction(self.colorMPTrillium) - self.menuOptions.addSeparator() - self.menuOptions.addAction(self.addPositionsToET) - self.menuOptions.addSeparator() - self.menuOptions.addAction(self.readAntellopeLog) - self.menuOptions.addSeparator() - self.menuOptions.addAction(self.showYYYYDOYDates) - self.menuOptions.addAction(self.showYYYY_MM_DDDates) - self.menuOptions.addAction(self.showYYYYMMMDDDates) - self.menuOptions.addSeparator() - self.menuOptions.addAction(self.setFontSizes) - self.menuHelp.addAction(self.openCalendar) - self.menuHelp.addAction(self.openAbout) - self.menuPlots.addAction(self.plotDSPClkDifference) - self.menuPlots.addAction(self.plotPhaseError) - self.menuPlots.addAction(self.plotJerk) - self.menuPlots.addAction(self.plotFileErrors) - self.menuPlots.addAction(self.plotGPSOnOffErr) - self.menuPlots.addAction(self.plotGPSLkUnlk) - self.menuPlots.addAction(self.plotTemperature) - self.menuPlots.addAction(self.plotVoltage) - self.menuPlots.addAction(self.plotBackupVoltage) - self.menuPlots.addAction(self.plotDumpCall) - self.menuPlots.addAction(self.plotAcquisitionOnOff) - self.menuPlots.addAction(self.plotResetPowerUp) - self.menuPlots.addAction(self.plotErrorWarning) - self.menuPlots.addAction(self.plotDescrepancies) - self.menuPlots.addAction(self.plotSOHDataDefinitions) - self.menuPlots.addAction(self.plotNetworkUpDown) - self.menuPlots.addAction(self.plotEvents) - self.menuPlots.addAction(self.plotDisk1Usage) - self.menuPlots.addAction(self.plotDisk2Usage) - self.menuPlots.addAction(self.plotMassPositions123) - self.menuPlots.addAction(self.plotMassPositions456) - self.menuPlots.addSeparator() - self.menuPlots.addAction(self.plotAll) - self.menubar.addAction(self.menuFile.menuAction()) - self.menubar.addAction(self.menuCommand.menuAction()) - self.menubar.addAction(self.menuPlots.menuAction()) - self.menubar.addAction(self.menuOptions.menuAction()) - self.menubar.addAction(self.menuForm.menuAction()) - self.menubar.addAction(self.menuHelp.menuAction()) - self.timeFromLabel.setBuddy(self.timeFromDateEdit) - self.timeToLabel.setBuddy(self.timeToDateEdit) - self.dsLabel.setBuddy(self.ds1CheckBox) - self.massPosLabel.setBuddy(self.lowChanCheckBox) - self.label_4.setBuddy(self.fileListLogCheckBox) - self.backgroundLabel.setBuddy(self.backgroundWhiteRadioButton) - - self.retranslateUi(MainWindow) - QtCore.QObject.connect(self.clearPushButton, QtCore.SIGNAL("clicked()"), self.searchLineEdit.clear) - QtCore.QObject.connect(self.quit, QtCore.SIGNAL("triggered(bool)"), MainWindow.close) - QtCore.QObject.connect(self.readPushButton, QtCore.SIGNAL("clicked()"), MainWindow.readSelectedFile) - QtCore.QObject.connect(self.stopPushButton, QtCore.SIGNAL("clicked()"), MainWindow.stopFileRead) - QtCore.QObject.connect(self.writePushButton, QtCore.SIGNAL("clicked()"), MainWindow.writePSFile) - # QtCore.QObject.connect(self.replotPushButton, QtCore.SIGNAL("clicked()"), self.plottingWidget.replotLoadedData) - QtCore.QObject.connect(self.reloadPushButton, QtCore.SIGNAL("clicked()"), MainWindow.reloadFile) - QtCore.QObject.connect(self.cwdPushButton, QtCore.SIGNAL("clicked()"), MainWindow.changeCurrentDirectory) - QtCore.QObject.connect(MainWindow, QtCore.SIGNAL("currentDirectoryChanged(QString)"), self.cwdLineEdit.setText) - QtCore.QMetaObject.connectSlotsByName(MainWindow) - - def retranslateUi(self, MainWindow): - MainWindow.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "MainWindow", None, -1)) - self.cwdPushButton.setText(QtWidgets.QApplication.translate("MainWindow", "Main Data Directory", None, -1)) - self.timeFromLabel.setText(QtWidgets.QApplication.translate("MainWindow", "From", None, -1)) - self.timeToLabel.setText(QtWidgets.QApplication.translate("MainWindow", "To", None, -1)) - self.ds6CheckBox.setText(QtWidgets.QApplication.translate("MainWindow", "6", None, -1)) - self.ds3CheckBox.setText(QtWidgets.QApplication.translate("MainWindow", "3", None, -1)) - self.dsTPSCheckBox.setText(QtWidgets.QApplication.translate("MainWindow", "TPS", None, -1)) - self.dsLabel.setText(QtWidgets.QApplication.translate("MainWindow", "DSs:", None, -1)) - self.ds2CheckBox.setText(QtWidgets.QApplication.translate("MainWindow", "2", None, -1)) - self.ds1CheckBox.setText(QtWidgets.QApplication.translate("MainWindow", "1", None, -1)) - self.ds4CheckBox.setText(QtWidgets.QApplication.translate("MainWindow", "4", None, -1)) - self.ds5checkBox.setText(QtWidgets.QApplication.translate("MainWindow", "5", None, -1)) - self.massPosLabel.setText(QtWidgets.QApplication.translate("MainWindow", "Mass Pos:", None, -1)) - self.hiChanCheckbox.setText(QtWidgets.QApplication.translate("MainWindow", "456", None, -1)) - self.lowChanCheckBox.setText(QtWidgets.QApplication.translate("MainWindow", "123", None, -1)) - self.writePushButton.setText(QtWidgets.QApplication.translate("MainWindow", "Write .ps", None, -1)) - self.stopPushButton.setText(QtWidgets.QApplication.translate("MainWindow", "Stop", None, -1)) - self.readPushButton.setText(QtWidgets.QApplication.translate("MainWindow", "Read", None, -1)) - self.sohCheckBox.setText(QtWidgets.QApplication.translate("MainWindow", "SOH Only", None, -1)) - self.searchLineEdit.setPlaceholderText(QtWidgets.QApplication.translate("MainWindow", "Search...", None, -1)) - self.fileListLogCheckBox.setText(QtWidgets.QApplication.translate("MainWindow", ".log", None, -1)) - self.fileListZipCheckBox.setText(QtWidgets.QApplication.translate("MainWindow", ".zip", None, -1)) - self.fileListCfCheckBox.setText(QtWidgets.QApplication.translate("MainWindow", ".cf", None, -1)) - self.fileListRefCheckBox.setText(QtWidgets.QApplication.translate("MainWindow", ".ref", None, -1)) - self.label_4.setText(QtWidgets.QApplication.translate("MainWindow", "List:", None, -1)) - self.backgroundWhiteRadioButton.setText(QtWidgets.QApplication.translate("MainWindow", "B", None, -1)) - self.backgroundBlackRadioButton.setText(QtWidgets.QApplication.translate("MainWindow", "W", None, -1)) - self.backgroundLabel.setText(QtWidgets.QApplication.translate("MainWindow", "Background:", None, -1)) - self.clearPushButton.setText(QtWidgets.QApplication.translate("MainWindow", "Clear", None, -1)) - self.replotPushButton.setText(QtWidgets.QApplication.translate("MainWindow", "Replot", None, -1)) - self.reloadPushButton.setText(QtWidgets.QApplication.translate("MainWindow", "Reload", None, -1)) - self.menuFile.setTitle(QtWidgets.QApplication.translate("MainWindow", "&File", None, -1)) - self.menuCommand.setTitle(QtWidgets.QApplication.translate("MainWindow", "&Commands", None, -1)) - self.menuExport_TT_Times_As.setTitle(QtWidgets.QApplication.translate("MainWindow", "&Export TT Times As...", None, -1)) - self.menuOptions.setTitle(QtWidgets.QApplication.translate("MainWindow", "&Options", None, -1)) - self.menuHelp.setTitle(QtWidgets.QApplication.translate("MainWindow", "&Help", None, -1)) - self.menuForm.setTitle(QtWidgets.QApplication.translate("MainWindow", "F&orms", None, -1)) - self.menuPlots.setTitle(QtWidgets.QApplication.translate("MainWindow", "&Plots", None, -1)) - self.quit.setText(QtWidgets.QApplication.translate("MainWindow", "&Quit", None, -1)) - self.deleteSetup.setText(QtWidgets.QApplication.translate("MainWindow", "&Delete Setup File", None, -1)) - self.openGPSPlots.setText(QtWidgets.QApplication.translate("MainWindow", "&GPS Plotter", None, -1)) - self.searchLog.setText(QtWidgets.QApplication.translate("MainWindow", "Log &Search", None, -1)) - self.plotTimeRanges.setText(QtWidgets.QApplication.translate("MainWindow", "Plot &Time Ranges", None, -1)) - self.plotPositions.setText(QtWidgets.QApplication.translate("MainWindow", "Plot &Positions", None, -1)) - self.exportDeploymentFile.setText(QtWidgets.QApplication.translate("MainWindow", "Deployment File (&Line)", None, -1)) - self.exportDeploymentBlock.setText(QtWidgets.QApplication.translate("MainWindow", "Deployment File (&Block)", None, -1)) - self.exportTSPGeometry.setText(QtWidgets.QApplication.translate("MainWindow", "&TSP Shotfile / Geometry", None, -1)) - self.exportShotInfo.setText(QtWidgets.QApplication.translate("MainWindow", "&Shot Info", None, -1)) - self.openCalendar.setText(QtWidgets.QApplication.translate("MainWindow", "&Calendar", None, -1)) - self.plotDSPClkDifference.setText(QtWidgets.QApplication.translate("MainWindow", "DSP-Clk Difference", None, -1)) - self.plotPhaseError.setText(QtWidgets.QApplication.translate("MainWindow", "Phase Error", None, -1)) - self.plotJerk.setText(QtWidgets.QApplication.translate("MainWindow", "Jerk/DSP Sets", None, -1)) - self.plotFileErrors.setText(QtWidgets.QApplication.translate("MainWindow", ".err File Errors", None, -1)) - self.plotGPSOnOffErr.setText(QtWidgets.QApplication.translate("MainWindow", "GPS On/Off/Err", None, -1)) - self.plotGPSLkUnlk.setText(QtWidgets.QApplication.translate("MainWindow", "GPS Lk-Unlk", None, -1)) - self.plotTemperature.setText(QtWidgets.QApplication.translate("MainWindow", "Temperature", None, -1)) - self.plotVoltage.setText(QtWidgets.QApplication.translate("MainWindow", "Volts", None, -1)) - self.plotBackupVoltage.setText(QtWidgets.QApplication.translate("MainWindow", "Backup Volts", None, -1)) - self.plotDumpCall.setText(QtWidgets.QApplication.translate("MainWindow", "Dump Call", None, -1)) - self.plotAcquisitionOnOff.setText(QtWidgets.QApplication.translate("MainWindow", "Acquisition On/Off", None, -1)) - self.plotResetPowerUp.setText(QtWidgets.QApplication.translate("MainWindow", "Reset/Powerup", None, -1)) - self.plotErrorWarning.setText(QtWidgets.QApplication.translate("MainWindow", "Error/Warning", None, -1)) - self.plotDescrepancies.setText(QtWidgets.QApplication.translate("MainWindow", "Discrepancies", None, -1)) - self.plotSOHDataDefinitions.setText(QtWidgets.QApplication.translate("MainWindow", "SOH/Data Definitions", None, -1)) - self.plotNetworkUpDown.setText(QtWidgets.QApplication.translate("MainWindow", "Network Up/Down", None, -1)) - self.plotEvents.setText(QtWidgets.QApplication.translate("MainWindow", "Events", None, -1)) - self.plotDisk1Usage.setText(QtWidgets.QApplication.translate("MainWindow", "Disk 1 Usage", None, -1)) - self.plotDisk2Usage.setText(QtWidgets.QApplication.translate("MainWindow", "Disk 2 Usage", None, -1)) - self.plotMassPositions123.setText(QtWidgets.QApplication.translate("MainWindow", "Mass Positions 123", None, -1)) - self.plotMassPositions456.setText(QtWidgets.QApplication.translate("MainWindow", "Mass Positions 456", None, -1)) - self.plotAll.setText(QtWidgets.QApplication.translate("MainWindow", "All Plots", None, -1)) - self.plotTimingProblems.setText(QtWidgets.QApplication.translate("MainWindow", "Plot Timing Problems", None, -1)) - self.filterNonSOHLines.setText(QtWidgets.QApplication.translate("MainWindow", "Filter Non-SOH Lines", None, -1)) - self.sortFilesByType.setText(QtWidgets.QApplication.translate("MainWindow", "Sort Files List By Type", None, -1)) - self.sortFilesAlphabetically.setText(QtWidgets.QApplication.translate("MainWindow", "Sort Files List Alphabetically", None, -1)) - self.calculateFileSizes.setText(QtWidgets.QApplication.translate("MainWindow", "Calculate File Sizes", None, -1)) - self.warnIfBig.setText(QtWidgets.QApplication.translate("MainWindow", "Warn If Big", None, -1)) - self.addMassPositionToSOH.setText(QtWidgets.QApplication.translate("MainWindow", "Add Mass Positions to SOH Messages", None, -1)) - self.colorMPRegular.setText(QtWidgets.QApplication.translate("MainWindow", "MP Coloring (Regular)", None, -1)) - self.colorMPTrillium.setText(QtWidgets.QApplication.translate("MainWindow", "MP Coloring (Trillium)", None, -1)) - self.addPositionsToET.setText(QtWidgets.QApplication.translate("MainWindow", "Add Positions to ET Lines", None, -1)) - self.readAntellopeLog.setText(QtWidgets.QApplication.translate("MainWindow", "Read Antelope-Produced Log File", None, -1)) - self.showYYYYDOYDates.setText(QtWidgets.QApplication.translate("MainWindow", "Show YYYY:DOY Dates", None, -1)) - self.showYYYY_MM_DDDates.setText(QtWidgets.QApplication.translate("MainWindow", "Show YYYY-MM-DD Dates", None, -1)) - self.showYYYYMMMDDDates.setText(QtWidgets.QApplication.translate("MainWindow", "Show YYYYMMMDD Dates", None, -1)) - self.setFontSizes.setText(QtWidgets.QApplication.translate("MainWindow", "Set Font Sizes", None, -1)) - self.openAbout.setText(QtWidgets.QApplication.translate("MainWindow", "&About", None, -1)) - -# from plottingwidget import PlottingWidget + hLayout.addWidget(self.timeToDateEdit) + + def setSecondRow(self, mainLayout): + hLayout = QtWidgets.QHBoxLayout() + hLayout.setContentsMargins(0, 0, 0, 0) + + mainLayout.addLayout(hLayout) + + self.setControlColumn(hLayout) + + self.plottingWidget = PlottingWidget( + self.MainWindow) + hLayout.addWidget(self.plottingWidget, 2) + + def setControlColumn(self, parentLayout): + # TODO: search for sideScrollArea + leftWidget = QtWidgets.QWidget(self.centralWidget) + leftWidget.setFixedWidth(240) + leftWidget.setMinimumHeight(650) + parentLayout.addWidget(leftWidget) + + leftLayout = QtWidgets.QVBoxLayout() + leftLayout.setContentsMargins(0, 0, 0, 0) + leftLayout.setSpacing(0) + leftWidget.setLayout(leftLayout) + + self.openFilesList = QtWidgets.QListWidget( + self.centralWidget) + leftLayout.addWidget(self.openFilesList, 1) + pal = self.openFilesList.palette() + pal.setColor(QtGui.QPalette.Highlight, QtGui.QColor(128, 255, 128)) + pal.setColor(QtGui.QPalette.HighlightedText, QtGui.QColor(0, 0, 0)) + self.openFilesList.setPalette(pal) + # allow multiple-line selection + self.openFilesList.setSelectionMode( + QtWidgets.QAbstractItemView.ExtendedSelection) + + self.insideDirCheckBox = QtWidgets.QCheckBox( + 'Inside Directory', self.centralWidget) + leftLayout.addWidget(self.insideDirCheckBox) + + searchGrid = QtWidgets.QGridLayout() + # searchGrid.setContentsMargins(0, 0, 0, 0) + leftLayout.addLayout(searchGrid) + + self.searchLineEdit = QtWidgets.QLineEdit(self.centralWidget) + self.searchLineEdit.setPlaceholderText('Search...') + searchGrid.addWidget(self.searchLineEdit, 0, 0, 1, 3) + + self.clearButton = QtWidgets.QPushButton('Clear', self.centralWidget) + self.clearButton.setFixedWidth(65) + searchGrid.addWidget(self.clearButton, 0, 3, 1, 1) + + searchGrid.addWidget(QtWidgets.QLabel('List:'), 1, 0, 1, 1) + self.fileListLogCheckBox = QtWidgets.QCheckBox( + '.log', self.centralWidget) + searchGrid.addWidget(self.fileListLogCheckBox, 1, 1, 1, 1) + self.fileListZipCheckBox = QtWidgets.QCheckBox( + '.zip', self.centralWidget) + searchGrid.addWidget(self.fileListZipCheckBox, 1, 2, 1, 1) + + self.replotButton = QtWidgets.QPushButton('RePlot', self.centralWidget) + self.replotButton.setFixedWidth(65) + searchGrid.addWidget(self.replotButton, 1, 3, 1, 1) + + backgroundLayout = QtWidgets.QHBoxLayout() + # backgroundLayout.setContentsMargins(0, 0, 0, 0) + leftLayout.addLayout(backgroundLayout) + backgroundLayout.addWidget(QtWidgets.QLabel('Background: ')) + self.backgroundBlackRadioButton = QtWidgets.QRadioButton( + 'B', self.centralWidget) + backgroundLayout.addWidget(self.backgroundBlackRadioButton) + self.backgroundWhiteRadioButton = QtWidgets.QRadioButton( + 'W', self.centralWidget) + backgroundLayout.addWidget(self.backgroundWhiteRadioButton) + + self.addSeperationLine(leftLayout) + + TPS_gap_SOH_Layout = QtWidgets.QGridLayout() + TPS_gap_SOH_Layout.setContentsMargins(0, 0, 0, 10) + TPS_gap_SOH_Layout.setSpacing(5) + leftLayout.addLayout(TPS_gap_SOH_Layout) + + self.showTPSCheckBox = QtWidgets.QCheckBox( + 'TPS Chans:', self.centralWidget) + TPS_gap_SOH_Layout.addWidget(self.showTPSCheckBox, 0, 0) + # TPS_gap_SOH_Layout.addWidget( + # QtWidgets.QLabel('Chans:'), 0, 1, QtGui.Qt.AlignRight) + self.TPSChansLineEdit = QtWidgets.QLineEdit(self.centralWidget) + TPS_gap_SOH_Layout.addWidget(self.TPSChansLineEdit, 0, 1, 1, 3) + + self.detectGapCheckBox = QtWidgets.QCheckBox( + 'DetectGap Len:', self.centralWidget) + TPS_gap_SOH_Layout.addWidget(self.detectGapCheckBox, 1, 0, 1, 2) + + # TPS_gap_SOH_Layout.addWidget( + # QtWidgets.QLabel('Len:'), 1, 2, QtGui.Qt.AlignRight) + self.gapLenLineEdit = QtWidgets.QLineEdit(self.centralWidget) + TPS_gap_SOH_Layout.addWidget(self.gapLenLineEdit, 1, 2, 1, 2) + + self.sohCheckBox = QtWidgets.QCheckBox('SOH Only', self.centralWidget) + TPS_gap_SOH_Layout.addWidget(self.sohCheckBox, 2, 0) + + massPosLayout = QtWidgets.QHBoxLayout() + # massPosLayout.setContentsMargins(0, 0, 0, 0) + leftLayout.addLayout(massPosLayout) + massPosLayout.addWidget(QtWidgets.QLabel('Mass Pos:')) + self.massPos123CheckBox = QtWidgets.QCheckBox( + '123', self.centralWidget) + massPosLayout.addWidget(self.massPos123CheckBox) + self.massPos456CheckBox = QtWidgets.QCheckBox( + '456', self.centralWidget) + massPosLayout.addWidget(self.massPos456CheckBox) + + self.addSeperationLine(leftLayout) + + dsGrid = QtWidgets.QGridLayout() + # dsGrid.setContentsMargins(0, 0, 0, 0) + leftLayout.addLayout(dsGrid) + + dsGrid.addWidget(QtWidgets.QLabel('DSs:'), 0, 0, 2, 1, + QtGui.Qt.AlignVCenter) + self.dsCheckBoxes = [] + count = 0 + for r in range(2): + for c in range(4): + count += 1 + self.dsCheckBoxes.append( + QtWidgets.QCheckBox('%s' % count, self.centralWidget)) + dsGrid.addWidget(self.dsCheckBoxes[count - 1], r, c + 1) + + self.addSeperationLine(leftLayout) + + chanLayout = QtWidgets.QHBoxLayout() + chanLayout.setContentsMargins(0, 0, 0, 0) + chanLayout.setSpacing(0) + leftLayout.addLayout(chanLayout) + + self.allChanCheckBox = QtWidgets.QCheckBox( + 'AllChan ', self.centralWidget) + self.allChanCheckBox.setChecked(True) + chanLayout.addWidget(self.allChanCheckBox) + + self.preferChanButton = QtWidgets.QPushButton( + 'Pref', self.centralWidget) + self.preferChanButton.setFixedWidth(47) + chanLayout.addWidget(self.preferChanButton) + + chanLayout.addWidget(QtWidgets.QLabel('Cur')) + self.currIDsNameLineEdit = QtWidgets.QLineEdit(self.centralWidget) + chanLayout.addWidget(self.currIDsNameLineEdit) + + submitLayout = QtWidgets.QHBoxLayout() + # submitLayout.setContentsMargins(5, 0, 0, 5) + submitLayout.setSpacing(5) + leftLayout.addLayout(submitLayout) + self.readButton = QtWidgets.QPushButton('Read', self.centralWidget) + submitLayout.addWidget(self.readButton) + self.stopButton = QtWidgets.QPushButton('Stop', self.centralWidget) + submitLayout.addWidget(self.stopButton) + self.writePSButton = QtWidgets.QPushButton( + 'Write .ps', self.centralWidget) + submitLayout.addWidget(self.writePSButton) + + self.infoListWidget = QtWidgets.QListWidget(self.centralWidget) + leftLayout.addWidget(self.infoListWidget, 1) + + def createMenuBar(self, MainWindow): + mainMenu = MainWindow.menuBar() + fileMenu = mainMenu.addMenu("File") + commandMenu = mainMenu.addMenu("Commands") + optionMenu = mainMenu.addMenu("Options") + self.formMenu = mainMenu.addMenu("Forms") + databaseMenu = mainMenu.addMenu("Database") + helpMenu = mainMenu.addMenu("Help") + + # exitAction = QtWidgets.QAction(QtGui.QIcon('exit.png'), "Exit", self) + # exitAction.setShortcut("Ctrl+X") + self.createFileMenu(MainWindow, fileMenu) + self.createCommandMenu(MainWindow, commandMenu) + self.createOptionMenu(MainWindow, optionMenu) + self.createDatabaseMenu(MainWindow, databaseMenu) + self.createHelpMenu(MainWindow, helpMenu) + + def createFileMenu(self, MainWindow, menu): + self.exitAction = QtWidgets.QAction('Close', MainWindow) + menu.addAction(self.exitAction) + + def createCommandMenu(self, MainWindow, menu): + self.GPSPlotterAction = QtWidgets.QAction( + 'GPS Plotter', MainWindow) + menu.addAction(self.GPSPlotterAction) + + self.logSearchAction = QtWidgets.QAction( + 'Log Search', MainWindow) + menu.addAction(self.logSearchAction) + + self.plotTimeRangesAction = QtWidgets.QAction( + 'Plot Time Ranges', MainWindow) + menu.addAction(self.plotTimeRangesAction) + + self.plotPositionAction = QtWidgets.QAction( + 'Log Search', MainWindow) + menu.addAction(self.plotPositionAction) + + exportTTTimesAsMenu = QtWidgets.QMenu( + 'Export TT Times As:', MainWindow) + menu.addMenu(exportTTTimesAsMenu) + + self.exportDeployFileLineAction = QtWidgets.QAction( + 'Deployment File (Line)', MainWindow) + exportTTTimesAsMenu.addAction(self.exportDeployFileLineAction) + + self.exportDeployFileBlockAction = QtWidgets.QAction( + 'Deployment File (Block)', MainWindow) + exportTTTimesAsMenu.addAction(self.exportDeployFileBlockAction) + + self.exportTPSShotFileAction = QtWidgets.QAction( + 'TPS Shotfile/Geometry', MainWindow) + exportTTTimesAsMenu.addAction(self.exportTPSShotFileAction) + + self.exportShotInfoAction = QtWidgets.QAction( + 'Shot Info', MainWindow) + exportTTTimesAsMenu.addAction(self.exportShotInfoAction) + + def createOptionMenu(self, MainWindow, menu): + self.plotTimingProblemsAction = QtWidgets.QAction( + 'Plot Timing Problems', MainWindow) + self.plotTimingProblemsAction.setCheckable(True) + menu.addAction(self.plotTimingProblemsAction) + + self.filterNonSOHAction = QtWidgets.QAction( + 'Filter Out Non-SOH Lines', MainWindow) + self.filterNonSOHAction.setCheckable(True) + menu.addAction(self.filterNonSOHAction) + + menu.addSeparator() + + Q330GainMenu = QtWidgets.QMenu('Q330 Gain:', MainWindow) + menu.addMenu(Q330GainMenu) + self.Q330LowGainAction = QtWidgets.QAction('Low', MainWindow) + self.Q330LowGainAction.setCheckable(True) + Q330GainMenu.addAction(self.Q330LowGainAction) + self.Q330HighGainAction = QtWidgets.QAction('High', MainWindow) + self.Q330HighGainAction.setCheckable(True) + Q330GainMenu.addAction(self.Q330HighGainAction) + + self.addMPToSOHAction = QtWidgets.QAction( + 'Add Mass Positions to SOH Messages', MainWindow) + self.addMPToSOHAction.setCheckable(True) + menu.addAction(self.addMPToSOHAction) + + MPColoringMenu = QtWidgets.QMenu('MP Coloring:', MainWindow) + menu.addMenu(MPColoringMenu) + self.MPRegularColorAction = QtWidgets.QAction( + '0.5, 2.0, 4.0, 7.0 (Regular)', MainWindow) + self.MPRegularColorAction.setCheckable(True) + MPColoringMenu.addAction(self.MPRegularColorAction) + self.MPTrilliumColorAction = QtWidgets.QAction( + '0.5, 1.8, 2.4, 3.5 (Trillium)', MainWindow) + self.MPTrilliumColorAction.setCheckable(True) + MPColoringMenu.addAction(self.MPTrilliumColorAction) + + menu.addSeparator() + + self.addPositionsToETAction = QtWidgets.QAction( + 'Add Positions to ET Lines', MainWindow) + self.addPositionsToETAction.setCheckable(True) + menu.addAction(self.addPositionsToETAction) + + self.readAntLogAction = QtWidgets.QAction( + 'Read Antelope-Produced Log Files', MainWindow) + self.readAntLogAction.setCheckable(True) + menu.addAction(self.readAntLogAction) + + menu.addSeparator() + + self.calcFileSizesAction = QtWidgets.QAction( + 'Calculate File Sizes', MainWindow) + menu.addAction(self.calcFileSizesAction) + self.warnIfBigAction = QtWidgets.QAction( + 'Warn If Big', MainWindow) + menu.addAction(self.warnIfBigAction) + + menu.addSeparator() + + dateFormatMenu = QtWidgets.QMenu('Date Format:', MainWindow) + menu.addMenu(dateFormatMenu) + self.YYYYDOYAction = QtWidgets.QAction( + 'YYYY:DOY', MainWindow) + self.YYYYDOYAction.setCheckable(True) + dateFormatMenu.addAction(self.YYYYDOYAction) + self.YYYY_MM_DDAction = QtWidgets.QAction( + 'YYYY-MM-DD', MainWindow) + self.YYYY_MM_DDAction.setCheckable(True) + dateFormatMenu.addAction(self.YYYY_MM_DDAction) + self.YYYYMMMDDAction = QtWidgets.QAction( + 'YYYYMMMDD', MainWindow) + self.YYYYMMMDDAction.setCheckable(True) + dateFormatMenu.addAction(self.YYYYMMMDDAction) + + menu.addSeparator() + + self.setFontSizeAction = QtWidgets.QAction( + 'Set Font Sizes', MainWindow) + menu.addAction(self.setFontSizeAction) + + def createDatabaseMenu(self, MainWindow, menu): + self.addEditDataTypeAction = QtWidgets.QAction( + 'Add/Edit Data Types', MainWindow) + menu.addAction(self.addEditDataTypeAction) + + self.addEditParamAction = QtWidgets.QAction( + 'Add/Edit Parameters', MainWindow) + menu.addAction(self.addEditParamAction) + + self.addEditChannelAction = QtWidgets.QAction( + 'Add/Edit Channels', MainWindow) + menu.addAction(self.addEditChannelAction) + + self.viewPlotTypeAction = QtWidgets.QAction( + 'View Plot Types', MainWindow) + menu.addAction(self.viewPlotTypeAction) + + def createHelpMenu(self, MainWindow, menu): + self.calendarAction = QtWidgets.QAction( + 'Calendar', MainWindow) + menu.addAction(self.calendarAction) + + self.aboutAction = QtWidgets.QAction( + 'About', MainWindow) + menu.addAction(self.aboutAction) + + def addSeperationLine(self, layout): + label = QtWidgets.QLabel() + label.setFrameStyle(QtWidgets.QFrame.HLine | QtWidgets.QFrame.Sunken) + label.setLineWidth(1) + layout.addWidget(label) + + def resizeEvent(self, event): + self.plottingWidget.init_size() + + def connectSignals(self, MainWindow): + self.connectMenuSignals(MainWindow) + self.connectWidgetSignals(MainWindow) + + def connectMenuSignals(self, MainWindow): + # File + self.exitAction.triggered.connect(MainWindow.close) + + # Commands + + # Options + self.MPRegularColorAction.triggered.connect( + lambda: setattr(MainWindow, 'massPosVoltRangeOpt', 'regular')) + self.MPTrilliumColorAction.triggered.connect( + lambda: setattr(MainWindow, 'massPosVoltRangeOpt', 'trillium')) + + self.YYYY_MM_DDAction.triggered.connect( + lambda: self.setDateFormat('yyyy-MM-dd')) + self.YYYYMMMDDAction.triggered.connect( + lambda: self.setDateFormat('yyyyMMMdd')) + self.YYYYDOYAction.triggered.connect( + lambda: self.setDateFormat('yyyyDOY')) + + # Database + self.addEditDataTypeAction.triggered.connect( + MainWindow.openDataType) + self.addEditParamAction.triggered.connect( + MainWindow.openParam) + self.addEditChannelAction.triggered.connect( + MainWindow.openChannel) + self.viewPlotTypeAction.triggered.connect( + MainWindow.openPlotType) + + # Form + + # Help + self.calendarAction.triggered.connect(MainWindow.openCalendarWidget) + + def connectWidgetSignals(self, MainWindow): + MainWindow.currentDirectoryChanged.connect(self.cwdLineEdit.setText) + # first Row + self.cwdButton.clicked.connect(MainWindow.changeCurrentDirectory) + self.timeToDateEdit.setCalendarWidget(CalendarWidget(MainWindow)) + self.timeToDateEdit.setDate(QtCore.QDate.currentDate()) + + self.timeFromDateEdit.setCalendarWidget(CalendarWidget(MainWindow)) + self.timeFromDateEdit.setDate(QtCore.QDate.currentDate()) + + # second Row + self.openFilesList.itemDoubleClicked.connect( + MainWindow.openFilesListItemDoubleClicked) + + self.replotButton.clicked.connect(MainWindow.replotLoadedData) + + self.allChanCheckBox.clicked.connect( + MainWindow.allChanClicked) + self.preferChanButton.clicked.connect( + MainWindow.openChannelPreferences) + self.readButton.clicked.connect(MainWindow.readSelectedFiles)