diff --git a/sohstationviewer/conf/constants.py b/sohstationviewer/conf/constants.py index d27768d785c5935791658d0f0a79c8b214b5ee4a..69409d50330745a2ab4eab6dd4712a58b2bf8026 100644 --- a/sohstationviewer/conf/constants.py +++ b/sohstationviewer/conf/constants.py @@ -1 +1,24 @@ +# to calc min() HIGHEST_INT = 1E100 + +# warn user if file bigger than this size +BIG_FILE_SIZE = 10**8 + +# Perfomance will be slow if data point total > than this limit +# => downsampled +CHAN_SIZE_LIMIT = 10**7 + +# If total waveform datapoint > than this limit, not recalculate +RECAL_SIZE_LIMIT = 10**9 + +# rate of values to be cut of from means +CUT_FROM_MEAN_FACTOR = 0.1 + +# to split to time range (not use for now, but implemented in mseed) +FILE_PER_CHAN_LIMIT = 20 + +# default start time +DEFAULT_START_TIME = "1970-01-01" + +# TIME FORMAT according to Qt.ISODate +TM_FM = "%Y-%m-%d" diff --git a/sohstationviewer/conf/dbSettings.py b/sohstationviewer/conf/dbSettings.py index de734017dc7217de7e4c51ade5ef3f3d52dad9d9..2a913a89d55879d55d25e256f70905e66809b80d 100755 --- a/sohstationviewer/conf/dbSettings.py +++ b/sohstationviewer/conf/dbSettings.py @@ -3,14 +3,46 @@ import re """ seisRE: Seismic data channels' regex: First letter(Band Code): ABCDEFGHLMOPQRSTUV -Second letter (Instrument Code): GHLMN +Second letter (Instrument Code): GHLMN (remove GM - Alissa) Third letter (Orientation Code): ZNE123 => to check if the channel is seismic data: if conf['seisRE'].match(chan): """ -conf = { +dbConf = { 'dbpath': 'sohstationviewer/database/soh.db', - 'seisRE': re.compile('[A-HLM-V][GHLMN][ZNE123]'), + 'seisRE': re.compile('[A-HLM-V][HLN][ZNE123]'), + 'wfReq': re.compile('^[A-HLM-V\*]([HLN\*][ZNE123\*]?)?$'), # noqa: W605 + # key is last char of chan + 'seisLabel': {'1': 'NS', '2': 'EW', 'N': 'NS', 'E': 'EW', 'Z': 'V'}, # +0.2:Y - 'valColRE': re.compile('^\+?\-?[0-9]+\.?[0-9]?:[RYGMC]') # noqa: W605 + 'valColRE': re.compile('^\+?\-?[0-9]+\.?[0-9]?:[RYGMC]'), # noqa: W605 + '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' + ) + } } diff --git a/sohstationviewer/controller/plottingData.py b/sohstationviewer/controller/plottingData.py index 545be6195fade756195bbfeb29ceaf04dea5485f..e127117afca857c3856bbafd16f52a2f622d73dc 100755 --- a/sohstationviewer/controller/plottingData.py +++ b/sohstationviewer/controller/plottingData.py @@ -3,15 +3,14 @@ Functions that process data before plotting """ import math +import numpy as np 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]} @@ -55,7 +54,7 @@ def getMassposValueColors(rangeOpt, chan, cMode, errors, retType='str'): return valueColors -def formatTime(parent, time, dateMode, timeMode=None): +def formatTime(time, dateMode, timeMode=None): """ :param parent: parent GUI to display tracking info :param time: time to be format, can be UTCDateTime or epoch time @@ -77,10 +76,6 @@ def formatTime(parent, time, dateMode, timeMode=None): 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" @@ -88,7 +83,7 @@ def formatTime(parent, time, dateMode, timeMode=None): return ret -def getTitle(parent, setID, plottingData, dateMode): +def getTitle(staID, minTime, maxTime, dateMode): """ :param setID: (netID, statID, locID) :param plottingData: a ditionary including: @@ -99,14 +94,12 @@ def getTitle(parent, setID, plottingData, dateMode): latestUTC: the latest time of all channels :return: title for the plot """ - diff = plottingData['latestUTC'] - plottingData['earliestUTC'] + diff = maxTime - minTime 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"), + (staID, + formatTime(minTime, dateMode, "HH:MM:SS"), + formatTime(maxTime, dateMode, "HH:MM:SS"), round(hours, 2)) ) @@ -164,7 +157,7 @@ def getTimeTicks(earliest, latest, dateFormat, labelTotal): time += interval while time < latest: times.append(time) - timeLabel = formatTime(None, time, dateFormat, 'HH:MM:SS') + timeLabel = formatTime(time, dateFormat, 'HH:MM:SS') if mode == "DD" or mode == "D": timeLabel = timeLabel[:-9] elif mode == "H": @@ -181,7 +174,14 @@ def getTimeTicks(earliest, latest, dateFormat, labelTotal): return times, majorTimes, majorTimelabels -def getUnitBitweight(chanDB, bitweightOpt): +def getDayTicks(): + times = list(range(1, 24)) + majorTimes = [t for t in list(range(4, 24, 4))] + majorTimeLabels = ["%02d" % t for t in majorTimes] + return times, majorTimes, majorTimeLabels + + +def getUnitBitweight(chanDB, bitweightOpt, isWF=False): """ :param chanDB: channel's info got from database :param bitweightOpt: qpeek's OPTBitWeightRVar @@ -198,12 +198,14 @@ def getUnitBitweight(chanDB, bitweightOpt): 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 chanDB['channel'] == 'SEISMIC': if fixPoint != 0: unitBitweight = "{:.%sf}%s" % (fixPoint, unit) else: @@ -212,3 +214,14 @@ def getUnitBitweight(chanDB, bitweightOpt): else: unitBitweight = "{}%s" % unit return unitBitweight + + +def convertWFactor(cData, convertFactor): + """ + 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['decData'] = np.multiply(cData['data'], [convertFactor]) + return cData diff --git a/sohstationviewer/controller/processing.py b/sohstationviewer/controller/processing.py index 87173891420575428f2adc25bb60c646522f27fc..893406e0b88ea837347c223b35d074fe96aaefe0 100644 --- a/sohstationviewer/controller/processing.py +++ b/sohstationviewer/controller/processing.py @@ -1,8 +1,9 @@ import os import json import re + from obspy.core import read as read_ms -from obspy.io.reftek.core import Reftek130, Reftek130Exception +from obspy.io.reftek.core import Reftek130Exception from sohstationviewer.model.mseed.mseed import MSeed from sohstationviewer.model.reftek.reftek import RT130 @@ -10,7 +11,8 @@ from sohstationviewer.database.extractData import signatureChannels from sohstationviewer.controller.util import validateFile, displayTrackingInfo -def loadData(dataType, parent, listOfDir, reqInfoChans, reqDSs): +def loadData(dataType, parent, listOfDir, reqWFChans=[], reqSOHChans=[], + readStart=None, readEnd=None): """ Go through root dir and read all files in that dir and its subdirs """ @@ -18,27 +20,34 @@ def loadData(dataType, parent, listOfDir, reqInfoChans, reqDSs): for d in listOfDir: if dataObject is None: if dataType == 'RT130': - dataObject = RT130(parent, d, reqInfoChans, reqDSs=reqDSs) + dataObject = RT130( + parent, d, reqWFChans=reqWFChans, reqSOHChans=reqSOHChans, + readStart=readStart, readEnd=readEnd) else: try: - dataObject = MSeed(parent, d, reqInfoChans) + dataObject = MSeed( + parent, d, reqWFChans=reqWFChans, + reqSOHChans=reqSOHChans, + readStart=readStart, readEnd=readEnd) 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 dataObject.hasData(): + # continue # 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) + # raise Exception("No data can be read from ", d) + # TODO: will work with select more than one dir later + # else: + # dataObject.readDir(d) - return dataObject.plottingData + # return dataObject.plottingData + return dataObject def readChannels(parent, listOfDir): """ + TODO: have to solve this later for changes of data type Scan available channels for channel prefer dialog """ dataObject = None @@ -47,6 +56,8 @@ def readChannels(parent, listOfDir): # dataObject = Reftek.Reftek(parent, d) # if dataObject.hasData(): # continue + + # dataObject = MSeed_Text(parent, d, readChanOnly=True) dataObject = MSeed(parent, d, readChanOnly=True) if len(dataObject.channels) == 0: # If no data can be read from the first dir, throw exception @@ -64,25 +75,24 @@ def detectDataType(parent, listOfDir): + 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: + print("d:", d) dataType = "Unknown" for path, subdirs, files in os.walk(d): for fileName in files: - if not validateFile(path, fileName): + path2file = os.path.join(path, fileName) + if not validateFile(path2file, fileName): continue - ret = getDataTypeFromFile(os.path.join(path, fileName), - sign_chan_dataType_dict) + ret = getDataTypeFromFile(path2file, sign_chan_dataType_dict) if ret is not None: - dataType = ret + dataType, chan = ret break if dataType != "Unknown": break - dirDataTypeDict[d] = dataType + dirDataTypeDict[d] = (dataType, chan) dataTypeList = {d for d in dirDataTypeDict.values()} if len(dataTypeList) > 1: dirDataTypeStr = json.dumps(dirDataTypeDict) @@ -101,23 +111,16 @@ def detectDataType(parent, listOfDir): return list(dirDataTypeDict.values())[0] -def getDataTypeFromFile(filePath, sign_chan_dataType_dict): +def getDataTypeFromFile(path2file, sign_chan_dataType_dict): stream = None try: - stream = read_ms(os.path.join(filePath)) + stream = read_ms(path2file) except TypeError: return except Reftek130Exception: - pass - - if not stream: - try: - Reftek130.from_file(os.path.join(filePath)) - except (TypeError, Reftek130Exception): - return - return 'RT130' + return 'RT130', '_' for trace in stream: chan = trace.stats['channel'] if chan in sign_chan_dataType_dict.keys(): - return sign_chan_dataType_dict[chan] + return sign_chan_dataType_dict[chan], chan diff --git a/sohstationviewer/controller/util.py b/sohstationviewer/controller/util.py index 76e632bd63641f6e962c9ea31581e90cdc22f3fc..dab43adfc36d6452c7f877631ca8afc04bde5eee 100644 --- a/sohstationviewer/controller/util.py +++ b/sohstationviewer/controller/util.py @@ -4,18 +4,18 @@ from datetime import datetime from obspy import UTCDateTime -def validateFile(path, fileName): +def validateFile(path2file, 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)): + if not os.path.isfile(os.path.join(path2file)): return False return True -def displayTrackingInfo(parent, text, type='info'): - if parent is None: +def displayTrackingInfo(trackingBox, text, type='info'): + if trackingBox is None: print(f"{type}: {text}") return @@ -34,8 +34,23 @@ def displayTrackingInfo(parent, text, type='info'): %(text)s </div> </body>""" - parent.trackingInfoTextBrowser.setHtml(htmlText % msg) - parent.update() + trackingBox.setHtml(htmlText % msg) + # parent.update() + trackingBox.repaint() + + +def getDirSize(dir): + totalSize = 0 + totalFile = 0 + for path, subdirs, files in os.walk(dir): + for fileName in files: + if not validateFile(path, fileName): + continue + fp = os.path.join(path, fileName) + # print("file %s: %s" % (fp, os.path.getsize(fp))) + totalSize += os.path.getsize(fp) + totalFile += 1 + return totalSize, totalFile def getTime6(timeStr): @@ -95,6 +110,10 @@ def getVal(text): return float(re.search(REVal, text).group()) +def isBinaryStr(text): + return lambda b: bool(b.translate(None, text)) + + ###################################### # BEGIN: rtnPattern(In, Upper = False) # LIB:rtnPattern():2006.114 - Logpeek diff --git a/sohstationviewer/database/extractData.py b/sohstationviewer/database/extractData.py index 215e563879c081ff221edf10769ffed942a3700a..6b28f847664a78a080849de6e59f6128f3c8ab92 100755 --- a/sohstationviewer/database/extractData.py +++ b/sohstationviewer/database/extractData.py @@ -1,10 +1,7 @@ -from sohstationviewer.database.proccessDB import executeDB_dict -from sohstationviewer.conf.dbSettings import conf +from sohstationviewer.database.proccessDB import executeDB_dict -# key is last char of chan -SEIS_LABEL = {'1': 'NS', '2': 'EW', - 'N': 'NS', 'E': 'EW', 'Z': 'V'} +from sohstationviewer.conf.dbSettings import dbConf def getChanPlotInfo(orgChan, dataType): @@ -25,7 +22,7 @@ def getChanPlotInfo(orgChan, dataType): chan = 'DS?' if orgChan.startswith('Disk Usage'): chan = 'Disk Usage?' - if conf['seisRE'].match(chan): + if dbConf['seisRE'].match(chan): chan = 'SEISMIC' sql = ("SELECT channel, plotType, height, unit, linkedChan," @@ -36,7 +33,7 @@ def getChanPlotInfo(orgChan, dataType): else: sql = (f"{sql} WHERE channel='{chan}' and C.param=P.param" f" and dataType='{dataType}'") - print("SQL:", sql) + # print("SQL:", sql) chanInfo = executeDB_dict(sql) if len(chanInfo) == 0: @@ -44,7 +41,7 @@ def getChanPlotInfo(orgChan, dataType): 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]['label'] = dbConf['seisLabel'][orgChan[-1]] chanInfo[0]['channel'] = orgChan chanInfo[0]['label'] = ( @@ -61,6 +58,19 @@ def getChanPlotInfo(orgChan, dataType): return chanInfo[0] +def getWFPlotInfo(orgChan): + chanInfo = executeDB_dict( + "SELECT * FROM Parameters WHERE param='Seismic data'") + if orgChan.startswith("DS"): + chanInfo[0]['label'] = orgChan + else: + chanInfo[0]['label'] = orgChan + '-' + dbConf['seisLabel'][orgChan[-1]] + + chanInfo[0]['unit'] = '' + chanInfo[0]['channel'] = 'SEISMIC' + return chanInfo[0] + + def signatureChannels(): """ return the dict {channel: dataType} in which channel is unique for dataType diff --git a/sohstationviewer/database/proccessDB.py b/sohstationviewer/database/proccessDB.py index 19edc92afe84072cda2abb62a10e98a7d1f7ba7e..6533e82b16ef00d438b5c485851d05aa42ac090a 100755 --- a/sohstationviewer/database/proccessDB.py +++ b/sohstationviewer/database/proccessDB.py @@ -1,13 +1,13 @@ import sqlite3 -from sohstationviewer.conf.dbSettings import conf +from sohstationviewer.conf.dbSettings import dbConf def executeDB(sql): """ used for both execute and query data """ - conn = sqlite3.connect(conf['dbpath']) + conn = sqlite3.connect(dbConf['dbpath']) cur = conn.cursor() try: cur.execute(sql) @@ -24,7 +24,7 @@ def executeDB_dict(sql): """ query data and return rows in dictionary with fields as keys """ - conn = sqlite3.connect(conf['dbpath']) + conn = sqlite3.connect(dbConf['dbpath']) conn.row_factory = sqlite3.Row cur = conn.cursor() try: @@ -42,7 +42,7 @@ def trunc_addDB(table, sqls): truncate table and refill with new data """ try: - conn = sqlite3.connect(conf['dbpath']) + conn = sqlite3.connect(dbConf['dbpath']) cur = conn.cursor() cur.execute('BEGIN') cur.execute(f'DELETE FROM {table}') diff --git a/sohstationviewer/model/dataType.py b/sohstationviewer/model/dataType.py deleted file mode 100644 index c5c37ecc626a6b72ab296ed405e9e8827acc5d5e..0000000000000000000000000000000000000000 --- a/sohstationviewer/model/dataType.py +++ /dev/null @@ -1,102 +0,0 @@ -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/data_type_model.py b/sohstationviewer/model/data_type_model.py new file mode 100644 index 0000000000000000000000000000000000000000..c95065c82287654f97f511f4d1645b521620d686 --- /dev/null +++ b/sohstationviewer/model/data_type_model.py @@ -0,0 +1,76 @@ +import os + +from tempfile import mkdtemp +import shutil + +from sohstationviewer.controller.util import displayTrackingInfo +from sohstationviewer.conf import constants + + +class WrongDataTypeError(Exception): + def __init__(self, *args, **kwargs): + self.args = (args, kwargs) + + +class DataTypeModel(): + def __init__(self, trackingBox, folder, readChanOnly=False, + reqWFChans=[], reqSOHChans=[], + readStart=0, readEnd=constants.HIGHEST_INT, + *args, **kwargs): + self.trackingBox = trackingBox + self.dir = folder + self.reqSOHChans = reqSOHChans + self.reqWFChans = reqWFChans + self.readChanOnly = readChanOnly + + self.readStart = readStart # start time to read in epoch + self.readEnd = readEnd # end time to read in epoch + self.processingLog = [] # [(message, type)] + self.logData = {'TEXT': []} # 'TEXT': for text only file + self.waveformData = {} + self.SOHData = {} + self.massPosData = {} + self.dataTime = {} # (earliestepoch,latestepoch) for each station + + self.channels = set() + self.station = None + self.nets = set() + self.stats = set() + self.gaps = {} + self.timeRanges = {} + self.selectedStaID = None + # # channel with the longest traces total for each station + # self.maxTraceTotalChan = {} + self.tmpDir = mkdtemp() + # print("currentdir:", os.getcwd()) + # tmpDirName = 'datatmp' + # self.tmpDir = os.path.join(os.getcwd(), tmpDirName) + try: + os.mkdir(self.tmpDir) + except FileExistsError: + shutil.rmtree(self.tmpDir) + os.mkdir(self.tmpDir) + + # self.readDir(folder, readChanOnly) + + def __del__(self): + print("delete dataType Object") + try: + shutil.rmtree(self.tmpDir) + except OSError as e: + self.trackInfo( + "Error deleting %s : %s" % (self.tmpDir, e.strerror), + "error") + print("Error deleting %s : %s" % (self.tmpDir, e.strerror)) + print("finish deleting") + + def hasData(self): + if (len(self.logData) == 0 and len(self.SOHData) == 0 and + len(self.massPosData) == 0 and len(self.waveformData) == 0): + return False + return True + + def trackInfo(self, text, type): + displayTrackingInfo(self.trackingBox, text, type) + if type != 'info': + self.processingLog.append((text, type)) diff --git a/sohstationviewer/model/handling_data.py b/sohstationviewer/model/handling_data.py new file mode 100644 index 0000000000000000000000000000000000000000..2a953b97abb41cb745b5ce490a5d0b126a889c08 --- /dev/null +++ b/sohstationviewer/model/handling_data.py @@ -0,0 +1,538 @@ + +import os +import math +from struct import unpack + +import numpy as np +from obspy.core import Stream, read as read_ms +from PySide2 import QtWidgets + +from sohstationviewer.model.mseed.blockettes_reader import ( + readNextBlkt, ReadBlocketteError) +from sohstationviewer.conf.dbSettings import dbConf +from sohstationviewer.conf import constants +from sohstationviewer.model.reftek.from_rt2ms import core + + +def readSOHMSeed(path2file, fileName, + SOHStreams, logData, netsProbInFile, trackInfo): + """ + Read ms and add trace in self.streams to merge later + Or read log wrap in ms and add to logData under channel in ms header + """ + stream = read_ms(path2file) + + file = None + nets = set() + stats = set() + netStats = set() + chanIDs = set() + newnetID = None + for trace in stream: + netID = trace.stats['network'].strip() + if netID != "None": + nets.add(netID) + if len(nets) > 1: + for netProb in netsProbInFile: + netProbSet = set(netProb) + if nets.issubset(netProbSet) or netProbSet.issubset(nets): + newnetID = netsProbInFile[netProb] + if nets != netProbSet: + netsProbInFile[tuple(nets)] = newnetID + break + if newnetID is None: + msg = (f"There are more than one net in file {fileName}.\n" + "Please select one.") + msgBox = QtWidgets.QMessageBox() + msgBox.setText(msg) + netButtons = [] + _nets = sorted(list(nets)) + for net in _nets: + netButtons.append(msgBox.addButton( + net, QtWidgets.QMessageBox.ActionRole)) + msgBox.exec_() + + selectedIdx = netButtons.index(msgBox.clickedButton()) + newnetID = _nets[selectedIdx] + netsProbInFile[tuple(nets)] = newnetID + + for trace in stream: + if newnetID is not None: + # use one net ID for all traces in a file + trace.stats['network'] = newnetID + netID = trace.stats['network'].strip() + chanID = trace.stats['channel'].strip() + staID = trace.stats['station'].strip() + stats.add(staID) + chanIDs.add(chanID) + netStats.add((netID, staID)) + if trace.stats.mseed['encoding'] == 'ASCII': + file = readASCII( + path2file, file, staID, chanID, trace, logData, trackInfo) + else: + if staID not in SOHStreams: + SOHStreams[staID] = {} + if chanID not in SOHStreams[staID]: + SOHStreams[staID][chanID] = Stream() + SOHStreams[staID][chanID].append(trace) + + if file is not None: + file.close() + return {'nets': list(nets), + 'stats': sorted(list(stats)), + 'netStats': sorted(list(netStats)), + 'chanIDs': sorted(list(chanIDs)) + } + + +def readSOHTrace(trace): + tr = {} + tr['chanID'] = trace.stats['channel'] + tr['samplerate'] = trace.stats['sampling_rate'] + tr['startTmEpoch'] = trace.stats['starttime'].timestamp + tr['endTmEpoch'] = trace.stats['endtime'].timestamp + """ + trace time start with 0 => need to add with epoch starttime + times and data have type ndarray + """ + tr['times'] = trace.times() + trace.stats['starttime'].timestamp + tr['data'] = trace.data + return tr + + +def readMPTrace(trace): + tr = {} + tr['chanID'] = trace.stats['channel'] + tr['samplerate'] = trace.stats['sampling_rate'] + tr['startTmEpoch'] = trace.stats['starttime'].timestamp + tr['endTmEpoch'] = trace.stats['endtime'].timestamp + """ + trace time start with 0 => need to add with epoch starttime + times and data have type ndarray + """ + tr['times'] = trace.times() + trace.stats['starttime'].timestamp + # TODO: MP only has 4 different values, data can be simplified if too big + tr['data'] = np.round_(trace.data / 3276.7, 1) + return tr + + +def readWaveformTrace(trace, staID, chanID, tracesInfo, tmpDir): + """ + read data from Trace and save data to files to save mem for processing + since waveform data are big + """ + # gaps for SOH only for now + tr = {} + tr['samplerate'] = trace.stats['sampling_rate'] + tr['startTmEpoch'] = trace.stats['starttime'].timestamp + tr['endTmEpoch'] = trace.stats['endtime'].timestamp + """ + trace time start with 0 => need to add with epoch starttime + times and data have type ndarray + """ + times = trace.times() + trace.stats['starttime'].timestamp + data = trace.data + tr['size'] = times.size + + trIdx = len(tracesInfo) + tr['times_zf'] = tr['times_f'] = saveData2File( + tmpDir, 'times', staID, chanID, times, trIdx, tr['size']) + tr['data_zf'] = tr['data_f'] = saveData2File( + tmpDir, 'data', staID, chanID, data, trIdx, tr['size']) + return tr + + +def readWaveformMSeed(path2file, fileName, staID, chanID, + tracesInfo, dataTime, tmpDir): + """ + Read ms and add trace in self.streams to merge later + Or read log wrap in ms and add to logData under channel in ms header + """ + stream = read_ms(path2file) + for trace in stream: + tr = readWaveformTrace(trace, staID, chanID, tracesInfo, tmpDir) + dataTime[0] = min(tr['startTmEpoch'], dataTime[0]) + dataTime[1] = max(tr['endTmEpoch'], dataTime[1]) + tracesInfo.append(tr) + + +def readWaveformReftek(rt130, staID, readData, dataTime, tmpDir): + stream = core.Reftek130.to_stream( + rt130, + headonly=False, + verbose=False, + sort_permuted_package_sequence=True) + for trace in stream: + chanID = trace.stats['channel'] + samplerate = trace.stats['sampling_rate'] + if chanID not in readData: + readData[chanID] = { + "tracesInfo": [], + "samplerate": samplerate} + tracesInfo = readData[chanID]['tracesInfo'] + tr = readWaveformTrace(trace, staID, chanID, tracesInfo, tmpDir) + dataTime[0] = min(tr['startTmEpoch'], dataTime[0]) + dataTime[1] = max(tr['endTmEpoch'], dataTime[1]) + tracesInfo.append(tr) + + +def readASCII(path2file, file, staID, chanID, trace, logData, trackInfo): + # TODO: read this in MSEED HEADER + 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() + logText += textFromData + if textFromData != '': + logText += textFromData + else: + recLen = h.mseed['record_length'] + if file is None: + file = open(path2file, '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: + trackInfo(f"{staID} - {chanID}: {e.msg}", 'error') + + if staID not in logData: + logData[staID] = {} + if h.channel not in logData[staID]: + logData[staID][h.channel] = [] + logData[staID][h.channel].append(logText) + return file + + +def readText(path2file, fileName, textLogs, ): + """ + Read log file and add to logData under channel TEXT + """ + with open(path2file, 'r') as file: + try: + content = file.read() + except UnicodeDecodeError: + raise Exception("Can't process file: %s" % fileName, 'error') + + logText = "\n\n** STATE OF HEALTH: %s\n" % fileName + logText += content + textLogs.append(logText) + + return True + + +def saveData2File(tmpDir, tm_data, staID, chanID, + tr, trIdx, trSize, postfix=''): + """ + save time/data to file to free memory for processing + :param tmpDir: the temp dir to save file in + :param tm_data: "times"/"data" + :param staID: station id + :param chaID: channel id + :param tr: numpy array of trace data + :param trIdx: trace index + :param trSize: trace size + :param postfix: in case of decimating for zooming, + add Z at the end of filename + """ + memFileName = os.path.join(tmpDir, + f"{staID}-{chanID}-{tm_data}-{trIdx}{postfix}") + # print("memFileName:%s - %s" % (memFileName, trSize)) + memFile = np.memmap(memFileName, dtype='int64', mode='w+', + shape=(1, trSize)) + memFile[:] = tr[:] + del tr + del memFile + return memFileName + + +def checkChan(chanID, reqSOHChans, reqWFChans): + """ + Check if chanID is required + return type if pass, False if not required + """ + ret = checkWFChan(chanID, reqWFChans) + if ret[0] == 'WF': + if ret[1]: + return "WF" + else: + return False + if checkSOHChan(chanID, reqSOHChans): + return "SOH" + return False + + +def checkSOHChan(chanID, reqSOHChans): + """ + check if chanID is an SOH channel or mass position (mseed channel) + check if it required by user + no reqSOHChans means read all + """ + if reqSOHChans == []: + return True + if chanID in reqSOHChans: + return True + if 'EX?' in reqSOHChans and chanID.startswith('EX'): + if chanID[2] in ['1', '2', '3']: + return True + # if 'VM?' in reqSOHChans and chanID.startswith('VM'): + if chanID.startswith('VM'): + # always read mass position + # TODO: add reqMPChans + if chanID[2] in ['0', '1', '2', '3', '4', '5', '6']: + return True + return False + + +def checkWFChan(chanID, reqWFChans): + """ + Check if chanID is a waveform data and is required by user + TODO: check with more wild card reqWFChans + """ + wf = '' + hasChan = False + if dbConf['seisRE'].match(chanID): + wf = 'WF' + if reqWFChans == ['*']: + hasChan = True + if chanID in reqWFChans: + hasChan = True + return wf, hasChan + + +def sortData(dataDict): + """ + Sort data in 'tracesInfo' according to 'startTmEpoch' + """ + for staID in dataDict: + for chanID in dataDict[staID]['readData']: + tracesInfo = dataDict[staID]['readData'][chanID]['tracesInfo'] + tracesInfo = sorted( + tracesInfo, key=lambda i: i['startTmEpoch']) + + +def squash_gaps(gaps): + """ + :param gaps: list of gaps: (start, end, length) + :return: squased_gaps: all related gaps are squashed extending to + min start and max end + """ + squashed_gaps = [] + sgap_indexes = [] + for idx, g in enumerate(gaps): + if idx in sgap_indexes: + continue + squashed_gaps.append(g) + sgap_indexes.append(idx) + for idx_, g_ in enumerate(gaps): + if idx_ in sgap_indexes: + continue + if ((g[0] <= g_[0] <= g[1]) or (g[0] <= g_[1] <= g[1]) + or (g_[0] <= g[0] <= g_[1]) or (g_[0] <= g[1] <= g_[1])): + sgap_indexes.append(idx_) + squashed_gaps[-1][0] = min(g[0], g_[0]) + squashed_gaps[-1][1] = max(g[1], g_[1]) + else: + break + return squashed_gaps + + +def downsample(times, data, rq_points): + if times.size <= rq_points: + return times, data + dataMax = max(abs(data.max()), abs(data.min())) + dataMean = abs(data.mean()) + indexes = np.where( + abs(data - data.mean()) > + (dataMax - dataMean) * constants.CUT_FROM_MEAN_FACTOR) + times = times[indexes] + data = data[indexes] + # return constant_rate(times, data, rq_points) + return chunk_minmax(times, data, rq_points) + + +def constant_rate(times, data, rq_points): + if times.size <= rq_points: + return times, data + rate = int(times.size/rq_points) + if rate == 1: + return times, data + + indexes = np.arange(0, times.size, rate) + times = times[indexes] + data = data[indexes] + + return times, data + + +def chunk_minmax(times, data, rq_points): + x, y = times, data + final_points = 0 + if x.size <= rq_points: + final_points += x.size + return x, y + + if rq_points < 2: + return np.empty((1, 0)), np.empty((1, 0)) + + # Since grabbing the min and max from each + # chunk, need to div the requested number of points + # by 2. + size = rq_points // 2 + cs = math.ceil(times.size / size) + + if cs * size > times.size: + cs -= 1 + # Length of the trace is not divisible by the number of requested + # points. So split into an array that is divisible by the requested + # size, and an array that contains the excess. Downsample both, + # and combine. This case gives slightly more samples than + # the requested sample size, but not by much. + x0 = times[:cs * size] + y0 = data[:cs * size] + + x1 = times[cs * size:] + y1 = data[cs * size:] + + dx0, dy0 = downsample(x0, y0, rq_points) + + # right-most subarray is always smaller than + # the initially requested number of points. + dx1, dy1 = downsample(x1, y1, cs) + + dx = np.zeros(dx0.size + dx1.size) + dy = np.zeros(dy0.size + dy1.size) + + # print(dx0.size + dx1.size) + + dx[:dx0.size] = dx0 + dy[:dy0.size] = dy0 + + dx[dx0.size:] = dx1 + dy[dy0.size:] = dy1 + del x0 + del y0 + del x1 + del y1 + del times + del data + return dx, dy + + x = x.reshape(size, cs) + y = y.reshape(size, cs) + + imin = np.argmin(y, axis=1) + imax = np.argmax(y, axis=1) + + rows = np.arange(size) + + mask = np.zeros(shape=(size, cs), dtype=bool) + mask[rows, imin] = True + mask[rows, imax] = True + + dx = x[mask] + dy = y[mask] + return dx, dy + + +def trim_downsample_SOHChan(chan, startTm, endTm, firsttime): + """ + trim off non-included time from chan[orgTrace], downsample, and save to + chan[times], chan[data] and [logIdx] if the key exist + """ + # TODO, add logIdx to downsample if using reftex + # zoom in to the given time + tr = chan['orgTrace'] + + indexes = np.where((startTm <= tr['times']) & (tr['times'] <= endTm)) + chan['times'], chan['data'] = downsample( + tr['times'][indexes], tr['data'][indexes], + constants.CHAN_SIZE_LIMIT) + + +def trim_downsample_WFChan(chan, startTm, endTm, firsttime): + """ + trim off all chans with non included time + if totalSize of the rest chans > RECAL_SIZE_LIMIT => need to be downsampled + Read data from tr's filename, downsample the data + Return data for plotting + """ + if 'fulldata' in chan: + # data is small, already has full in the first trim + return + # zoom in to the given range + chan['startIdx'] = 0 + chan['endIdx'] = len(chan['tracesInfo']) + + if ((startTm > chan['tracesInfo'][-1]['endTmEpoch']) or + (endTm < chan['tracesInfo'][0]['startTmEpoch'])): + return False + + indexes = [index for index, tr in enumerate(chan['tracesInfo']) + if tr['startTmEpoch'] > startTm] + if indexes != []: + chan['startIdx'] = indexes[0] + if chan['startIdx'] > 0: + chan['startIdx'] -= 1 # startTm in middle of trace + else: + chan['startIdx'] = 0 + + indexes = [idx for (idx, tr) in enumerate(chan['tracesInfo']) + if tr['endTmEpoch'] <= endTm] + if indexes != []: + chan['endIdx'] = indexes[-1] + if chan['endIdx'] < len(chan['tracesInfo']) - 1: + chan['endIdx'] += 1 # endTm in middle of trace + else: + chan['endIdx'] = 0 + chan['endIdx'] += 1 # a[x:y+1] = [a[x], ...a[y] + + zTracesInfo = chan['tracesInfo'][chan['startIdx']:chan['endIdx']] + totalSize = sum([tr['size'] for tr in zTracesInfo]) + if not firsttime and totalSize > constants.RECAL_SIZE_LIMIT: + # size too big, recalc only when it's small enough + return + try: + del chan['times'] + del chan['data'] + except Exception: + pass + rq_points = 0 + if totalSize > constants.CHAN_SIZE_LIMIT: + rq_points = int(constants.CHAN_SIZE_LIMIT / len(zTracesInfo)) + elif firsttime: + chan['fulldata'] = True + + chan['times'] = [] + chan['data'] = [] + for trIdx, tr in enumerate(zTracesInfo): + times = np.memmap(tr['times_f'], + dtype='int64', mode='r', + shape=(1, tr['size'])) + data = np.memmap(tr['data_f'], + dtype='int64', mode='r', + shape=(1, tr['size'])) + indexes = np.where((startTm <= times) & (times <= endTm)) + times = times[indexes] + data = data[indexes] + if rq_points != 0: + times, data = downsample(times, data, rq_points) + chan['times'].append(times) + chan['data'].append(data) + + chan['times'] = np.hstack(chan['times']) + chan['data'] = np.hstack(chan['data']) + return + + +if __name__ == '__main__': + print(checkChan('HH1', [], [])) diff --git a/sohstationviewer/model/mseed/blockettes_reader.py b/sohstationviewer/model/mseed/blockettes_reader.py index a627fd3d4cccaad00c907a7982262313997d6385..71b96435928aaea4d51908d41f46196e19236479 100644 --- a/sohstationviewer/model/mseed/blockettes_reader.py +++ b/sohstationviewer/model/mseed/blockettes_reader.py @@ -50,13 +50,11 @@ def readNextBlkt(bNo, databytes, byteorder): try: # readBlkt will skip first 4 bytes (HH) as they are already read - info = eval("readBlkt%s(%s, %s, '%s')" % - (blocketteType, bNo, databytes, byteorder)) + 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__()") + raise ReadBlocketteError(f"Function to read blockette {blocketteType} " + f"isn't implemented yet.") return nextBNo, info @@ -117,7 +115,7 @@ def readBlkt2000(bNo, databytes, byteorder): opaqueDataLength = blktLen - 15 - headerLen logText += "\nOpaque Data: %s" % unpack( '%s%s' % (byteorder, '%ss' % opaqueDataLength), - databytes[n + headerLen: n + headerLen + opaqueDataLength]) + databytes[n + headerLen:n + headerLen + opaqueDataLength]) return logText diff --git a/sohstationviewer/model/mseed/from_mseedpeek/__init__.py b/sohstationviewer/model/mseed/from_mseedpeek/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/sohstationviewer/model/mseed/from_mseedpeek/mseed_header.py b/sohstationviewer/model/mseed/from_mseedpeek/mseed_header.py new file mode 100644 index 0000000000000000000000000000000000000000..1033828d377c0a35021cadf26be8936060f9bc7f --- /dev/null +++ b/sohstationviewer/model/mseed/from_mseedpeek/mseed_header.py @@ -0,0 +1,492 @@ +import struct +from sohstationviewer.controller.util import getTime6 +from sohstationviewer.model.handling_data import ( + readSOHMSeed, readText, checkChan) + + +class futils: + """ + file utilities class + """ + + def __init__(self, infile): + self.infile = open(infile, 'r+b') + + def close(self): + self.infile.close() + + def where(self): + return self.infile.tell() + + +######################################################### +class FixedHeader: + """ + mseed fixhdr + """ + # first 20char + textblk = timeblk = sampblk = miscblk = [] + textblk = [] + Serial = DHQual = Res = Stat = Chan = Loc = Net = None + # 20:30 + timeblk = [] + Year = Day = Hour = Min = Sec = Micro = None + # 30:36 + sampblk = [] + NumSamp = SampFact = SampMult = None + # 36:48 + miscblk = [] + act = io = DataDHQualFL = numblock = timcorr = bdata = bbblock = None + + +######################################################### +class MseedHeader(futils): + + def __init__(self, infile): + """ + initialize file, determine byteorder, sizes, time, and load first fixed + header + """ + self.type = self.rate = None + # try: + futils.__init__(self, infile) + # local variable to class + self.infileseek = self.infile.seek + self.infilewrite = self.infile.write + self.infileread = self.infile.read + self.sunpack = struct.unpack + self.spack = struct.pack + + self.byteorder = self.ByteOrder() + + if self.byteorder != "unknown": + # otherwise it might be mseed + self.type = "mseed" + # read 1st fixed header in file + self.fixedhdr() + (self.filesize, self.blksize) = self.sizes() + + # except Exception as e: + # print("e:",e) + # pass + + ######################################################### + + def ByteOrder(self, seekval=20): + """ + read file as if it is mseed just pulling time info + from fixed header and determine if it makes sense unpacked + as big endian or little endian + """ + Order = "unknown" + try: + # seek to timeblock and read + self.infileseek(seekval) + timeblock = self.infileread(10) + + # assume big endian + (Year, Day, Hour, Min, Sec, junk, Micro) = \ + self.sunpack('>HHBBBBH', timeblock) + # test if big endian read makes sense + if 1950 <= Year <= 2050 and \ + 1 <= Day <= 366 and \ + 0 <= Hour <= 23 and \ + 0 <= Min <= 59 and \ + 0 <= Sec <= 59: + Order = "big" + self.fmt_order = ">" + else: + # try little endian read + (Year, Day, Hour, Min, Sec, junk, Micro) = \ + self.sunpack('<HHBBBBH', timeblock) + # test if little endian read makes sense + if 1950 <= Year <= 2050 and \ + 1 <= Day <= 366 and \ + 0 <= Hour <= 23 and \ + 0 <= Min <= 59 and \ + 0 <= Sec <= 59: + Order = "little" + self.fmt_order = "<" + except Exception: + pass + + return Order + +######################################################### +# +# for blockette descriptions below +# from SEED manual +# +# Field nbits Description +# UBYTE 8 Unsigned quantity +# BYTE 8 Two's complement signed quantity +# UWORD 16 Unsigned quantity +# WORD 16 Two's complement signed quantity +# ULONG 32 Unsigned quantity +# LONG 32 Two's complement signed quantity +# CHAR*n n*8 n characters, each 8 bit and each with +# a 7-bit ASCII character (high bit always 0) +# FLOAT 32 IEEE Floating point number +# +# BTIME +# UWORD 16 Year (e.g. 1987) +# UWORD 16 J-Day +# UBYTE 8 Hours of day (0-23) +# UBYTE 8 Minutes of hour (0-59) +# UBYTE 8 Seconds of minute (0-59, 60 for leap seconds) +# UBYTE 8 Unused for data (required for alignment)( +# UWORD 16 .0001 seconds (0-9999) +######################################################### + def fixedhdr(self, seekval=0): + """ + Reads fixed header of 48 bytes (see SEED manual) + Returns four tuples + textblk (SeqNum, DHQual, res, Stat, Loc, Chan, Net) + Sequence Number (CHAR*6) + Data header/quality indicator (CHAR*1) + Reserved (CHAR*1) + Station identifier code (CHAR*5) + Location identifier (CHAR*2) + Channel identifier (CHAR*3) + Network Code (CHAR*2) + timeblk (Year, Day, Hour, Min, Sec, junk, Micro) + Year (UWORD, 2) + Jday (UWORD, 2) + Hour (UBYTE, 1) + Min (UBYTE, 1) + Sec (UBYTE, 1) + unused (UBYTE, 1) + .0001 seconds (0-9999) (UWORD, 2) + sampblk (NumSamp, SampFact, SampMult) + Number of samples (UWORD, 2) + Sample rate factor (see calcrate) (WORD, 2) + Sample rate multiplier (see calcrate) (WORD, 2) + miscblk (act, io, DataDHQualFl, numblock, timcorr, bdata, + bblock) + Activity flags (UBYTE, 1) + I/O and clock flags (UBYTE, 1) + Data quality flags (UBYTE, 1) + Number of blockettes that follow (UBYTE, 1) + Time correction (LONG, 4) + Offset to beginning of data (UWORD, 2) + Offset to beginning of next blockette (UWORD, 2) + """ + # local variable + # self.sunpack=self.sunpack + try: + del self.FH + except Exception: + pass + self.FH = FixedHeader() + try: + self.infileseek(seekval) + fhblk = self.infileread(48) + # station info + fmtstr = self.fmt_order + "6scc5s2s3s2s" + self.FH.textblk = self.sunpack(fmtstr, fhblk[0:20]) + + # time info + fmtstr = self.fmt_order + "HHBBBBH" + self.FH.timeblk = self.sunpack(fmtstr, fhblk[20:30]) + + # sample info + fmtstr = self.fmt_order + "Hhh" + self.FH.sampblk = self.sunpack(fmtstr, fhblk[30:36]) + + # misc info + fmtstr = self.fmt_order + "BBBBlHH" + tmpblk = self.sunpack(fmtstr, fhblk[36:48]) + # decompose tmpblk[0-2] into bit fields, create miscblk + for i in range(3): + self.FH.miscblk = self.UbytetoStr(tmpblk, i) + tmpblk = self.FH.miscblk # recast tmpblk as list + + (self.FH.Serial, self.FH.DHQual, res, self.FH.Stat, + self.FH.Loc, self.FH.Chan, self.FH.Net) = self.FH.textblk + (self.FH.Year, self.FH.Day, self.FH.Hour, self.FH.Min, + self.FH.Sec, junk, self.FH.Micro) = self.FH.timeblk + (self.FH.NumSamp, self.FH.SampFact, + self.FH.SampMult) = self.FH.sampblk + self.rate = self.calcrate() + + return (self.FH.textblk, self.FH.timeblk, self.FH.sampblk, + self.FH.miscblk) + + except Exception as e: + raise Exception("error reading fixed header: %s" % str(e)) + + def sizes(self, seekval=0): + """ + Finds Blockette 1000 and returns file size & Data Record Length + """ + # try: + # determine file size + self.infileseek(0, 2) + filesize = self.infile.tell() + # proceed to seekval + self.infileseek(seekval) + # self.infileseek(39) + # assign number of blockettes and offset to next blockette + nblock = self.FH.miscblk[3] + nextblock = self.FH.miscblk[6] + n = 0 + # find blockette 1000 + while n < nblock: + self.infileseek(nextblock) + (blktype, newblock) = self.typenxt(nextblock) + if not blktype: + return None, None + if blktype == 1000: + (type, next, self.encode, order, length, + res) = self.blk1000(nextblock) + return filesize, 2**length + nextblock = newblock + n += 1 + # except Exception: + # return None, None + + ######################################################### + + def UbytetoStr(self, tup, offset): + """ + converts unsign byte to string values + """ + list = [] + strval = "" + # mask bit fields and build string + for i in range(8): + mask = 2 ** i + if tup[offset] & mask: + strval = "1" + strval + else: + strval = "0" + strval + + # build new list with decomposed bit string + for i in range(len(tup)): + if i == offset: + list.append(strval) + else: + list.append(tup[i]) + return list + + ######################################################### + + def calcrate(self): + """ + this routine assumes that idread has been called first + + calculate the sample rate of mseed data + If Sample rate factor > 0 and Sample rate Multiplier > 0, + rate = Sampfact X Sampmult + If Sample rate factor > 0 and Sample rate Multiplier < 0, + rate = -1 X Sampfact/Sampmult + If Sample rate factor < 0 and Sample rate Multiplier > 0, + rate = -1 X Sampmult/Sampfact + If Sample rate factor < 0 and Sample rate Multiplier < 0, + rate = 1/(Sampfact X Sampmult) + """ + sampFact = float(self.FH.SampFact) + sampMult = float(self.FH.SampMult) + if sampFact > 0 and sampMult > 0: + rate = sampFact * sampMult + elif sampFact > 0 and sampMult < 0: + rate = -1.0 * (sampFact / sampMult) + elif sampFact < 0 and sampMult > 0: + rate = -1.0 * (sampMult / sampFact) + elif sampFact < 0 and sampMult < 0: + rate = 1.0 / (sampFact * sampMult) + else: + rate = sampFact + return rate + + ######################################################### + + def typenxt(self, seekval=0): + """ + Reads first 4 bytes of blockette + Returns blockette type and next blockette offset + """ + try: + self.infileseek(seekval) + fmtstr = self.fmt_order + "HH" + (type, next) = self.sunpack(fmtstr, self.infileread(4)) + + # reset back to beginning of blockette + self.infileseek(-4, 1) + return type, next + except Exception: + return None, None + + ######################################################### + + def blk1000(self, seekval=0): + """ + Data Only SEED Blockette 1000 (8 bytes) + Returns tuple + blk + Blockette type (UWORD, 2) + Next blockette's byte offset relative to fixed section of header + (UWORD, 2) + Encoding Format (BYTE, 1) + Word Order (UBYTE, 1) + Data Record Length (UBYTE, 1) + Reserved (UBYTE, 1) + """ + self.infileseek(seekval) + fmtstr = self.fmt_order + "HHbBBB" + blk = self.sunpack(fmtstr, self.infileread(8)) + return list(blk) + + ######################################################### + + def blk1001(self, seekval=0): + """ + Data Extension Blockette 1001 (8 bytes) + Returns tuple + blk + Blockette type (UWORD, 2) + Next blockette's byte offset relative to fixed section of header + (UWORD, 2) + Timing Quality (UBYTE, 1) + microsec (UBYTE, 1) + Reserved (UBYTE, 1) + Frame count (UBYTE, 1) + """ + self.infileseek(seekval) + fmtstr = self.fmt_order + "HHBBBB" + blk = self.sunpack(fmtstr, self.infileread(8)) + return list(blk) + + ######################################################### + + def blk500(self, seekval=0): + """ + Timing Blockette 500 (200 bytes) + Returns tuple + blk + Blockette type (UWORD, 2) + Next blockette's byte offset relative to fixed section of header + (UWORD, 2) + VCO correction (FLOAT, 4) + Time of exception (BTime expanded, 10) + microsec (UBYTE, 1) + Reception quality (UBYTE, 1) + Exception count (ULONG, 4) + Exception type (CHAR*16) + Clock model (CHAR*32) + Clock status (CHAR*128) + """ + self.infileseek(seekval) + fmtstr = self.fmt_order + "HHf" + blk1 = self.sunpack(fmtstr, self.infileread(8)) + fmtstr = self.fmt_order + "HHBBBBH" + timeblk = self.sunpack(fmtstr, self.infileread(10)) + fmtstr = self.fmt_order + "BBI16s32s128s" + blk2 = self.sunpack(fmtstr, self.infileread(182)) + + # incorporate timeblk tuple into blk list + blk = list(blk1) + blk.append(timeblk) + blk = blk + list(blk2) + + return blk + + ######################################################### + + def isMseed(self): + """ + determines if processed file is mseed (return 1) or unknown type + (return 0) + """ + # if we don't know byteorder it must not be mseed + if self.byteorder == "unknown": + return 0 + else: + return 1 + + +def readHdrs(path2file, fileName, + SOHStreams, logData, + reqSOHChans, reqWFChans, + netsProbInFile, trackInfo): + """ + read headers of a given file build dictionary for quick access + """ + + # create object (we've already tested all files for mseed) + # and get some base info + # print("fileName:", fileName) + rdfile = MseedHeader(path2file) + if rdfile.isMseed(): + try: + filesize = rdfile.filesize + blksize = rdfile.blksize + encode = rdfile.encode + chanID = rdfile.FH.Chan.strip().decode() + chanType = checkChan(chanID, reqSOHChans, reqWFChans) + if not chanType: + rdfile.close() + return + except Exception: + rdfile.close() + raise Exception("Cannot determine file and block sizes. File: %s" + % fileName) + else: + # not Mseed() + rdfile.close() + readText(path2file, fileName, logData['TEXT']) + return + + if chanType == 'SOH': + readSOHMSeed(path2file, fileName, SOHStreams, + logData, netsProbInFile, trackInfo) + return + if reqWFChans == []: + return + (numblocks, odd_size) = divmod(filesize, blksize) + nets = set() + stats = set() + netStats = set() + chanIDs = set() + epochs = [] + startTms = [] + gaps = [] + # looping over total number of blocks in files + for n in range(numblocks): + rdfile.fixedhdr(n * blksize) + + chanID = rdfile.FH.Chan.strip().decode() + + chanIDs.add(chanID) + nets.add(rdfile.FH.Net.strip().decode()) + stats.add(rdfile.FH.Stat.strip().decode()) + netStats.add((rdfile.FH.Net.strip().decode(), + rdfile.FH.Stat.strip().decode())) + + t_str = "%d:%03d:%02d:%02d:%02d:%06d" % ( + rdfile.FH.Year, rdfile.FH.Day, rdfile.FH.Hour, + rdfile.FH.Min, rdfile.FH.Sec, rdfile.FH.Micro) + startTms.append(t_str) + startepoch, _ = getTime6(t_str) + endepoch = None + if rdfile.rate != 0: + endepoch = startepoch + rdfile.FH.NumSamp / rdfile.rate + + epochs.append((startepoch, endepoch)) + + rdfile.close() + + return {'path2file': path2file, + 'fileName': fileName, + 'nets': list(nets), + 'stats': sorted(list(stats)), + 'netStats': sorted(list(netStats)), + 'chanIDs': sorted(list(chanIDs)), + 'gaps': gaps, + 'encode': encode, + 'startEpoch': epochs[0][0], + 'endEpoch': epochs[-1][1], + 'tracesTotal': len(epochs), + 'spr': rdfile.rate, + 'read': False + } diff --git a/sohstationviewer/model/mseed/mseed.py b/sohstationviewer/model/mseed/mseed.py index 41fc063e3d25bfb4aa8196a2606a1405a9c8559e..88ecb0fbafee7c8152daf361539c8c86b866e099 100644 --- a/sohstationviewer/model/mseed/mseed.py +++ b/sohstationviewer/model/mseed/mseed.py @@ -1,194 +1,280 @@ +""" +waveform: + indexing and read only the files in selected time, + + apply saving memory + + down sample traces before combine data + + data not use for plotting is kept in files to free memory + for processing. +SOH: merge all traces (with obspy stream) before down sample. +gaps: for SOH only, can manually calc. gaps for waveform but takes time + +""" + + import os -from obspy.core import Stream, read as read_ms -from obspy import UTCDateTime -from struct import unpack +from PySide2 import QtWidgets -from sohstationviewer.model.dataType import DataType +from sohstationviewer.view.selectbuttonsdialog import SelectButtonDialog +from sohstationviewer.model.data_type_model import DataTypeModel -from sohstationviewer.model.mseed.blockettes_reader import ( - readNextBlkt, ReadBlocketteError) from sohstationviewer.conf import constants +from sohstationviewer.model.mseed.from_mseedpeek.mseed_header import readHdrs +from sohstationviewer.controller.util import validateFile +from sohstationviewer.model.handling_data import ( + readWaveformMSeed, squash_gaps, checkWFChan, + sortData, readSOHTrace) -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): - self.readText(path, fileName) - - def combineData(self): - for k in self.streams: # k=netID, statID, locID - 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) - 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'] = minStarttime.timestamp - self.plottingData[k]['latestUTC'] = maxEndtime.timestamp - - def addLog(self, chan, logText): - if chan not in self.logData[self.curNetStatLoc].keys(): - self.logData[self.curNetStatLoc][chan] = [] - self.logData[self.curNetStatLoc][chan].append(logText) - - def readText(self, path, fileName): - """ - Read log file and add to logData under channel TEXT - """ - if self.readChanOnly and 'LOG' in self.channels: + +class MSeed(DataTypeModel): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # keys: 1+ nets values: net user choose + self.netsProbInFile = {} + self.readSOH_indexWaveform(self.dir) + self.selectedKey = self.selectStaID() + if self.selectedKey is None: 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 readMiniseed(self, path, fileName): - """ - Read ms and add trace in self.streams to merge later - Or read log wrap in ms and add to logData under channel in ms header - """ - try: - stream = read_ms(os.path.join(path, fileName)) - except TypeError: - return False - file = None - for trace in stream: - chanID = trace.stats['channel'] - if self.readChanOnly: - self.channels.add(chanID) - continue - if not self.checkChan(chanID): - self.noneReqChans.add(chanID) + if len(self.reqWFChans) != 0: + # self.reselectTimeRange(self.selectedKey) + self.readWFFiles(self.selectedKey) + + def readSOH_indexWaveform(self, folder): + netStatProb = {} + statProb = {} + chanProb = {} + count = 0 + SOHStreams = {} + for path, subdirs, files in os.walk(folder): + for fileName in files: + path2file = os.path.join(path, fileName) + if not validateFile(path2file, fileName): + continue + count += 1 + if count % 50 == 0: + self.trackInfo( + f'Read {count} file headers/ SOH files', 'info') + + ret = readHdrs( + path2file, fileName, SOHStreams, self.logData, + self.reqSOHChans, self.reqWFChans, + self.netsProbInFile, self.trackInfo) + if ret is None: + continue + + self.nets.update(ret['nets']) + self.stats.add(ret['stats'][0]) + + if len(ret['stats']) > 1: + k = tuple(ret['stats']) + if k not in statProb: + statProb[k] = [] + statProb[k].append(fileName) + else: + if len(ret['netStats']) > 1: + k = tuple(ret['netStats']) + if k not in netStatProb: + netStatProb[k] = [] + netStatProb[k].append(fileName) + if len(ret['chanIDs']) > 1: + k = tuple(ret['chanIDs']) + if k not in chanProb: + chanProb[k] = [] + chanProb[k].append(fileName) + self.channels.update(ret['chanIDs']) + if 'read' not in ret: + # not waveform + continue + + # TODO currently for waveform not read the file + # with 2 stations, 2 channels + # correctly. Use the first one in the file only + # When have time will dig more to this + staID = ret['stats'][0] + chanID = ret['chanIDs'][0] + if staID not in self.waveformData: + self.waveformData[staID] = {"filesInfo": {}, + "readData": {}} + self.dataTime[staID] = [constants.HIGHEST_INT, 0] + if chanID not in self.waveformData[staID]: + self.waveformData[staID]["filesInfo"][chanID] = [] + self.waveformData[staID]["readData"][chanID] = { + "samplerate": ret['spr'], + "tracesInfo": []} + self.waveformData[staID]['filesInfo'][chanID].append(ret) + + self.channels = sorted(list(self.channels)) + self.nets = sorted(list(self.nets)) + self.stats = sorted(list(self.stats)) + + if len(statProb) > 0: + errmsg = (f"More than one stations in a file: {statProb}. " + f"Will use the first one.") + self.trackInfo(errmsg, "error") + if len(netStatProb) > 0: + errmsg = "More than one netIDs in a file: %s" % netStatProb + self.trackInfo(errmsg, "warning") + if len(chanProb) > 0: + errmsg = (f"More than one channels in a file: {chanProb} " + f"\nThis is a CRITICAL ERROR.") + self.trackInfo(errmsg, "error") + # merge SOHStreams - squash gaps - gaps for SOH only + allGaps = [] + for staID in SOHStreams: + self.dataTime[staID] = [constants.HIGHEST_INT, 0] + self.SOHData[staID] = {} + self.massPosData[staID] = {} + for chanID in SOHStreams[staID]: + stream = SOHStreams[staID][chanID] + + stream.merge() + if len(stream) > 1: + nets = [tr.stats['network'].strip() for tr in stream] + nets += [f"Combine to {n}" for n in nets] + msg = (f"There are more than one net for sta {staID}.\n" + "Please select one or combine all to one.") + msgBox = SelectButtonDialog(message=msg, + buttonLabels=nets) + msgBox.exec_() + selNet = nets[msgBox.ret] + if "Combine" not in selNet: + tr = [tr for tr in stream + if tr.stats['network'] == selNet][0] + else: + for tr in stream: + selNet = selNet.replace("Combine to ", "") + tr.stats['network'] = selNet + stream.merge() + tr = stream[0] + else: + tr = stream[0] + + gaps = stream.get_gaps() + allGaps += [[g[4].timestamp, g[5].timestamp] for g in gaps] + traceInfo = readSOHTrace(tr) + if chanID.startswith('VM'): + self.massPosData[staID][chanID] = {'orgTrace': traceInfo} + else: + self.SOHData[staID][chanID] = {'orgTrace': traceInfo} + self.dataTime[staID][0] = min(traceInfo['startTmEpoch'], + self.dataTime[staID][0]) + self.dataTime[staID][1] = max(traceInfo['endTmEpoch'], + self.dataTime[staID][1]) + + self.gaps[staID] = squash_gaps(allGaps) + + # For each staID, get chanID of the channel with + # the most total of files. Will consider files instead of trace for now + # for staID in self.waveformData: + # maxTraceTotal = 0 + # chanIdxs = self.waveformData[staID] + # for chanID in chanIdxs: + # traceTotal = len(chanIdxs[chanID]) + # if traceTotal > maxTraceTotal: + # maxTraceTotal = traceTotal + # maxChanID = self.maxTraceTotalChan[staID] = chanID + # for chanID in spr0Chans: + # chanIdxs[chanID][-1]['endEpoch'] = chanIdxs[ + # maxChanID][-1]['endEpoch'] + + def selectStaID(self): + selectedStaID = self.stats[0] + if len(self.stats) > 1: + msg = ("There are more than one stations in the given data.\n" + "Please select one to one to display") + msgBox = QtWidgets.QMessageBox() + msgBox.setText(msg) + staButtons = [] + for staID in self.stats: + staButtons.append(msgBox.addButton( + staID, QtWidgets.QMessageBox.ActionRole)) + abortButton = msgBox.addButton(QtWidgets.QMessageBox.Abort) + + msgBox.exec_() + + if msgBox.clickedButton() == abortButton: + return selectedStaID + selectedIdx = staButtons.index(msgBox.clickedButton()) + selectedStaID = self.stats[selectedIdx] + self.trackInfo(f'Select Station {selectedStaID}', 'info') + return selectedStaID + # + # def reselectTimeRange(self, staID): + # """ + # If there are no time ranges created for the station, create them + # Provide a dialog for user to choose a time range. Each button is + # a time range. User click one, time range will be changed in + # MainWindow (TODO), the dialog will be closed. If necessary, + # will change the way to do this later. + # """ + # if staID not in self.timeRanges: + # chanID = self.maxTraceTotalChan[staID] + # filesInfo = self.waveformData[staID][chanID]['filesInfo'] + # cutTrace = [ + # tr for tr in filesInfo + # if (self.readStart <= tr['startEpoch'] < self.readEnd) or + # (self.readStart < tr['endEpoch'] <= self.readEnd)] + # cutTraceTotal = len(cutTrace) + # print(f"{chanID } cutTraceTotal: {cutTraceTotal}") + # traceTotalDiv = int( + # cutTraceTotal / constants.FILE_PER_CHAN_LIMIT) + # if traceTotalDiv < 2: + # # readStart, readEnd remain unchanged + # return + # self.timeRanges[staID] = [] + # FILE_LIM = constants.FILE_PER_CHAN_LIMIT + # for i in range(traceTotalDiv): + # startTm = cutTrace[i * FILE_LIM]['startEpoch'] + # if i < (traceTotalDiv - 1): + # endTm = cutTrace[i * FILE_LIM + FILE_LIM - 1]['endEpoch'] + # else: + # endTm = cutTrace[-1]['endEpoch'] + # self.timeRanges[staID].append((startTm, endTm)) + # msg = "Data in the selected time is too big to display.\n" + # else: + # msg = "" + # + # msg += "Please choose one of the suggested time range below." + # tmStrList = [] + # for tm in self.timeRanges[staID]: + # startTm = UTCDateTime(int(tm[0])).strftime("%Y-%m-%d %H:%M") + # endTm = UTCDateTime(int(tm[1])).strftime("%Y-%m-%d %H:%M") + # tmStrList.append("%s-%s" % (startTm, endTm)) + # msgBox = SelectButtonDialog(message=msg, buttonLabels=tmStrList) + # msgBox.exec_() + # + # self.readStart, self.readEnd = self.timeRanges[staID][msgBox.ret] + # self.trackInfo(f"Select time {self.readStart, self.readEnd}", "info") + + def readWFFiles(self, staID): + count = 0 + for chanID in self.waveformData[staID]['filesInfo']: + # check chanID + hasChan = checkWFChan(chanID, self.reqWFChans) + if not hasChan: continue - netID = trace.stats['network'] - statID = trace.stats['station'] - locID = trace.stats['location'] - self.curNetStatLoc = k = (netID, statID, locID) - if trace.stats.mseed['encoding'] == 'ASCII': - file = self.readASCII(path, fileName, file, trace, k, chanID) - else: - if k not in self.streams.keys(): - self.streams[k] = Stream() - self.streams[k].append(trace) - if file is not None: - file.close() - return True - - 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 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 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 - - 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 + tracesInfo = self.waveformData[staID][ + 'readData'][chanID]['tracesInfo'] + + for fileInfo in self.waveformData[staID]['filesInfo'][chanID]: + # file have been read + if fileInfo['read']: + continue + + # check time + hasData = False + + if ((self.readStart <= fileInfo['startEpoch'] <= self.readEnd) or # noqa: E501 + (self.readStart <= fileInfo['endEpoch'] <= self.readEnd)): # noqa: E501 + hasData = True + if not hasData: + continue + readWaveformMSeed(fileInfo['path2file'], fileInfo['fileName'], + staID, chanID, tracesInfo, + self.dataTime[staID], self.tmpDir) + fileInfo['read'] = True + count += 1 + if count % 50 == 0: + self.trackInfo(f'Read {count} waveform files', 'info') + + sortData(self.waveformData) diff --git a/sohstationviewer/model/reftek/logInfo.py b/sohstationviewer/model/reftek/logInfo.py index 1947dbe7adf2717999bbe1feab5e0f36cb86f81d..dd9f8caa9aabe7d3b78ac060e39d5e9b0a66b2d4 100644 --- a/sohstationviewer/model/reftek/logInfo.py +++ b/sohstationviewer/model/reftek/logInfo.py @@ -1,16 +1,15 @@ from sohstationviewer.conf import constants from sohstationviewer.controller.util import ( - displayTrackingInfo, getTime6, getTime4, getVal, - rtnPattern) + getTime6, getTime4, getVal, rtnPattern) class LogInfo(): - def __init__(self, parent, parentGUI, logText, key, packetType, reqDSs, + def __init__(self, parent, trackInfo, logText, key, packetType, reqDSs, isLogFile=False): self.packetType = packetType self.parent = parent - self.parentGUI = parentGUI + self.trackInfo = trackInfo self.logText = logText self.key = key self.unitID, self.expNo = key @@ -30,7 +29,7 @@ class LogInfo(): self.model = "72A" self.maxEpoch = 0 self.minEpoch = constants.HIGHEST_INT - self.chans = self.parent.plottingData[self.key]['channels'] + self.chans = self.parent.SOHData[self.key] self.CPUVers = set() self.GPSVers = set() self.extractInfo() @@ -76,7 +75,7 @@ class LogInfo(): "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') + self.trackInfo(msg, 'error') False return epoch @@ -210,15 +209,16 @@ class LogInfo(): def addChanInfo(self, chanName, t, d, idx): if chanName not in self.chans: - self.chans[chanName] = { + self.chans[chanName] = {} + self.chans[chanName]['orgTrace'] = { '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) + self.chans[chanName]['orgTrace']['times'].append(t) + self.chans[chanName]['orgTrace']['data'].append(d) + self.chans[chanName]['orgTrace']['logIdx'].append(idx) def extractInfo(self): diff --git a/sohstationviewer/model/reftek/reftek.py b/sohstationviewer/model/reftek/reftek.py index 48e3a4e891c4f1e88e579f61c8da5fcff220cb45..36c77b8e4ad22085af642f47f4115e8ea727a1c3 100755 --- a/sohstationviewer/model/reftek/reftek.py +++ b/sohstationviewer/model/reftek/reftek.py @@ -2,28 +2,105 @@ import os import numpy as np -from obspy import UTCDateTime +from PySide2 import QtWidgets 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.model.data_type_model import DataTypeModel +from sohstationviewer.model.handling_data import ( + readWaveformReftek, squash_gaps, sortData, readMPTrace, readText) + from sohstationviewer.conf import constants -from sohstationviewer.model.dataType import DataType +from sohstationviewer.controller.util import validateFile -class RT130(DataType): +class RT130(DataTypeModel): def __init__(self, *args, **kwarg): self.EH = {} super().__init__(*args, **kwarg) + self.keys = set() + self.reqDSs = self.reqWFChans + self.massPosStream = {} + self.readSOH_indexWaveform(self.dir) - 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) + self.selectedKey = self.selectKey() + if self.selectedKey is None: + return + if len(self.reqWFChans) != 0: + self.readWFFiles(self.selectedKey) + + def readSOH_indexWaveform(self, dir): + count = 0 + for path, subdirs, files in os.walk(dir): + for fileName in files: + path2file = os.path.join(path, fileName) + if not validateFile(path2file, fileName): + continue + if not self.readReftek130(path2file, fileName): + readText(path2file, fileName, self.logData['TEXT']) + count += 1 + if count % 50 == 0: + self.trackInfo( + f'Read {count} file headers/ SOH files', 'info') + + self.combineData() + + def selectKey(self): + self.keys = sorted(list(self.keys)) + selectedKey = self.keys[0] + if len(self.keys) > 1: + msg = ("There are more than one keys in the given data.\n" + "Please select one to one to display") + msgBox = QtWidgets.QMessageBox() + msgBox.setText(msg) + staButtons = [] + for key in self.keys: + staButtons.append(msgBox.addButton( + key, QtWidgets.QMessageBox.ActionRole)) + abortButton = msgBox.addButton(QtWidgets.QMessageBox.Abort) + + msgBox.exec_() + + if msgBox.clickedButton() == abortButton: + return selectedKey + selectedIdx = staButtons.index(msgBox.clickedButton()) + selectedKey = self.keys[selectedIdx] + self.trackInfo(f'Select Key {selectedKey}', 'info') + return selectedKey + + def readWFFiles(self, staID): + count = 0 + for DS in self.waveformData[staID]['filesInfo']: + readData = self.waveformData[staID]['readData'] + for fileInfo in self.waveformData[staID]['filesInfo'][DS]: + # file have been read + if fileInfo['read']: + continue + + # check time + hasData = False + if ((self.readStart <= fileInfo['startEpoch'] <= self.readEnd) or # noqa: E501 + (self.readStart <= fileInfo['endEpoch'] <= self.readEnd)): # noqa: E501 + hasData = True + if not hasData: + continue + readWaveformReftek(fileInfo['rt130'], staID, readData, + self.dataTime[staID], self.tmpDir) + fileInfo['read'] = True + count += 1 + if count % 50 == 0: + self.trackInfo(f'Read {count} waveform files', 'info') + sortData(self.waveformData) + + 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) + + def readReftek130(self, path2file, fileName): rt130 = core.Reftek130.from_file(path2file) unique, counts = np.unique(rt130._data["packet_type"], return_counts=True) @@ -32,10 +109,25 @@ class RT130(DataType): 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) + self.readEHET_MP_indexWF(rt130) return True - def readEHET(self, rt130): + 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)) + if 'SOH' not in self.logData[self.curKey]: + self.logData[self.curKey]['SOH'] = [] + self.logData[self.curKey]['SOH'].append((d['time'], logs)) + + def readEHET_MP_indexWF(self, rt130): DS = rt130._data['data_stream_number'][0] + 1 if DS not in self.reqDSs + [9]: return @@ -55,69 +147,61 @@ class RT130(DataType): 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)) + if 'EHET' not in self.logData[self.curKey]: + self.logData[self.curKey]['EHET'] = [] + self.logData[self.curKey]['EHET'].append((d['time'], logs)) + if self.curKey not in self.dataTime: + self.dataTime[self.curKey] = [constants.HIGHEST_INT, 0] + self.keys.add(self.curKey) + if DS == 9: + self.readMassPos(rt130) + else: + self.indexWaveForm(rt130, DS) + + def readMassPos(self, rt130): + if self.curKey not in self.massPosStream: + + self.massPosStream[self.curKey] = Stream() 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) + for tr in stream: + self.massPosStream[self.curKey].append(tr) + self.dataTime[self.curKey][0] = min( + tr.stats['starttime'].timestamp, self.dataTime[self.curKey][0]) + self.dataTime[self.curKey][1] = max( + tr.stats['endtime'].timestamp, self.dataTime[self.curKey][1]) + + def indexWaveForm(self, rt130, DS): + if self.curKey not in self.waveformData: + self.waveformData[self.curKey] = {"filesInfo": {}, + "readData": {}} + if DS not in self.waveformData[self.curKey]["filesInfo"]: + self.waveformData[self.curKey]["filesInfo"][DS] = [] - 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'] + stream = core.Reftek130.to_stream( + rt130, + headonly=True, + verbose=False, + sort_permuted_package_sequence=True) + + self.waveformData[self.curKey]["filesInfo"][DS].append( + {'rt130': rt130, + 'startEpoch': stream[0].stats['starttime'].timestamp, + 'endEpoch': stream[0].stats['endtime'].timestamp, + 'read': False}) def combineData(self): for k in self.logData: - self.plottingData[k] = { - 'gaps': [], - 'channels': {} - } + if k == 'TEXT': + continue + if k not in self.dataTime: + self.dataTime[k] = [constants.HIGHEST_INT, 0] + self.keys.add(k) + self.SOHData[k] = {} logs = [] for pktType in ['SOH', 'EHET']: if pktType == 'EHET': @@ -134,77 +218,27 @@ class RT130(DataType): 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] + self, self.trackInfo, logStr, k, pktType, self.reqDSs) + self.dataTime[k][0] = min(logObj.minEpoch, self.dataTime[k][0]) + self.dataTime[k][1] = max(logObj.maxEpoch, self.dataTime[k][1]) + for cName in self.SOHData[k]: + c = self.SOHData[k][cName]['orgTrace'] 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] + # look in mseed to do this merge + allGaps = [] + for k in self.massPosStream: + self.massPosData[k] = {} + stream = self.massPosStream[k] stream.merge() + for tr in stream: + chanID = tr.stats['channel'] + self.massPosData[k][chanID] = {} + self.massPosData[k][chanID]['orgTrace'] = readMPTrace(tr) + gaps = stream.get_gaps() # 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 + allGaps += [[g[4].timestamp, g[5].timestamp] for g in gaps] + self.gaps[k] = squash_gaps(allGaps) diff --git a/sohstationviewer/view/core/dbgui_superclass.py b/sohstationviewer/view/core/dbgui_superclass.py index f24870287d8dbe3c415f9133a922cb75cb44c675..45dc5189241ad68adceaf157f2bfbb9d1bf452aa 100755 --- a/sohstationviewer/view/core/dbgui_superclass.py +++ b/sohstationviewer/view/core/dbgui_superclass.py @@ -72,6 +72,7 @@ class Ui_DBInfoDialog(QtWidgets.QWidget): # "\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, diff --git a/sohstationviewer/view/core/plottingWidget.py b/sohstationviewer/view/core/plottingWidget.py index b95de5c7f7cb9e6c3100b73af4f4f4dff5dcac0d..63ccb35e18d543fbc78cd180f670b2774a20ced1 100755 --- a/sohstationviewer/view/core/plottingWidget.py +++ b/sohstationviewer/view/core/plottingWidget.py @@ -1,4 +1,3 @@ -import math from PySide2 import QtCore, QtWidgets from matplotlib.backends.backend_qt5agg import ( @@ -10,40 +9,17 @@ import numpy as np from sohstationviewer.controller.plottingData import ( getTitle, getGaps, getTimeTicks, getUnitBitweight, getMassposValueColors) +from sohstationviewer.controller.util import displayTrackingInfo, getVal + from sohstationviewer.conf import constants from sohstationviewer.conf.colorSettings import Clr, set_colors +from sohstationviewer.conf.dbSettings import dbConf + 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' - ) -} +BOTTOM = 0.996 +BOTTOM_PX = 200 class PlottingWidget(QtWidgets.QScrollArea): @@ -55,9 +31,11 @@ class PlottingWidget(QtWidgets.QScrollArea): gap, dots, : 3 """ - def __init__(self, parent=None): + def __init__(self, parent=None, name=''): super().__init__() + self.name = name self.parent = parent + self.peerPlottingWidgets = [self] self.plotNo = 0 self.infoWidget = None self.widgt = QtWidgets.QWidget(parent) @@ -65,9 +43,13 @@ class PlottingWidget(QtWidgets.QScrollArea): self.currplot_title = None self.hidden_plots = {} self.zoomMarker1Shown = False + print("set zoomMarker1Shown False %s" % self.name) self.axes = [] - self.widthBase = 0.185 + self.widthBase = 0.25 + self.widthBasePx = 1546 + self.ratioW = 1 + # width of plotting area self.plottingW = self.widthBase # X1: 0.19: Width = 20% of 50in (Figure's width) @@ -76,6 +58,13 @@ class PlottingWidget(QtWidgets.QScrollArea): # height of plotting area # + changed when plots are added or removed # + changed when changing the V-magnify param + + self.plottingBot = BOTTOM + # 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.plottingLBase = 0.04 + self.plottingLBase = self.plottingLBase self.plottingH = 0.996 # this is the height of a standard plot # plotH = 0.01 # 0.01: Height = 1% of 100in (Figure's height) @@ -86,6 +75,7 @@ class PlottingWidget(QtWidgets.QScrollArea): # 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. @@ -93,14 +83,30 @@ class PlottingWidget(QtWidgets.QScrollArea): 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.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') + # ============================= EVENT ============================== + def resizeEvent(self, event): + # print("resizeEvent") + geo = self.maximumViewportSize() + # print("resize geo:", geo) + + # set view size fit with the scroll's view port size + self.widgt.setFixedWidth(geo.width()) + self.ratioW = geo.width()/self.widthBasePx + self.plottingW = self.ratioW * self.widthBase + self.plottingL = self.ratioW * self.plottingLBase + if self.plotNo == 0: + self.widgt.setFixedHeight(geo.height()) + + return super(PlottingWidget, self).resizeEvent(event) + def contextMenuEvent(self, event): if self.axes == []: return @@ -121,7 +127,7 @@ class PlottingWidget(QtWidgets.QScrollArea): lambda *arg, index=i: self.show_hidden_plot(index)) contextMenu.exec_(self.mapToGlobal(event.pos())) - def __getTimestamp(self, event): + 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 @@ -129,22 +135,24 @@ class PlottingWidget(QtWidgets.QScrollArea): print('xdata of mouse: {:.2f}'.format(xdata)) return xdata - def __zoomBwMarkers(self, xdata): + def zoomBwMarkers(self, xdata): + print("%s zoomBwMarkers" % self.name) if self.currMinX == xdata: return # self.fig.canvas.mpl_disconnect(self.follower) - self.__draw() + self.draw() self.zoomMarker1Shown = False + print("set zoomMarker1Shown False %s" % self.name) [self.currMinX, self.currMaxX] = sorted( [self.currMinX, xdata]) - print("ZM2 self.currMinX:", self.currMinX) - print("ZM2 self.currMaxX:", self.currMaxX) - self.__set_lim() + # 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() + self.draw() - def __on_pick(self, event): + def on_pick(self, event): """ xmouse, ymouse = event.mouseevent.xdata, event.mouseevent.ydata # x, y = artist.get_xdata(), artist.get_ydata() @@ -163,46 +171,55 @@ class PlottingWidget(QtWidgets.QScrollArea): """ # 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() + xdata = self.getTimestamp(event) + for w in self.peerPlottingWidgets: + if modifiers == QtCore.Qt.ShiftModifier: + print(" %s shift+click" % w.name) + print("%s check zoomMarker1Shown: %s" + % (w.name, w.zoomMarker1Shown)) + if not w.zoomMarker1Shown: + w.ruler.set_visible(False) + w.set_ruler_visibled(w.zoomMarker1, xdata) + w.currMinX = xdata + # print("ZM1 self.currMinX:", self.currMinX) + w.zoomMarker1Shown = True + print("set zoomMarker1Shown True %s" % w.name) + w.set_ruler_visibled(w.zoomMarker2, xdata) + + w.draw() + else: + w.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("%s Ctrl+click" % w.name) + w.zoomMarker1.set_visible(False) + w.zoomMarker1Shown = False + print("set zoomMarker1Shown False %s" % w.name) + try: + w.fig.canvas.mpl_disconnect(w.follower) + except Exception: + pass + w.set_ruler_visibled(w.ruler, xdata) 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) + print("%s click xmouse: %s" % (w.name, xdata)) + if w.zoomMarker1Shown: + w.zoomBwMarkers(xdata) - def __on_pick_on_artist(self, event): - print("============__on_pick ============") + 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() @@ -232,43 +249,53 @@ class PlottingWidget(QtWidgets.QScrollArea): self.currplot_index = self.axes.index(artist) self.currplot_title = "Plot %s" % self.currplot_index - def __set_ruler_visibled(self, ruler, x): + def set_ruler_visibled(self, ruler, x): + print("%s set_ruler_visibled" % self.name) 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) + try: + if ruler == self.zoomMarker2: + print("\t=>%s connect zoomMarker2" % self.name) + # 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) + except Exception: + pass + self.draw() + + def zoomMarker2_follow_mouse(self, mouseevent): + # TODO: set zoomMarker2 for peerPlottingWidgets that not this current + # because for now zoomMarker2 has no interact in between + xdata = self.getTimestamp(mouseevent) self.zoomMarker2.xy1 = (xdata, 0) self.zoomMarker2.xy2 = (xdata, self.bottom) - self.__draw() + 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() + for w in self.peerPlottingWidgets: + w.ruler.set_visible(False) + w.zoomMarker1.set_visible(False) + w.zoomMarker2.set_visible(False) + w.zoomMarker1Shown = False + print("set zoomMarker1Shown False %s" % w.name) + w.draw() return super(PlottingWidget, self).keyPressEvent(event) + # ============================= END EVENT ============================== - def __add_timestamp_bar(self, usedHeight, top=True): + def add_timestamp_bar(self, usedHeight, top=True, hasLabel=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 + self.plottingBot -= usedHeight timestampBar = self.canvas.figure.add_axes( - [self.plottingL, self.plottingH, self.plottingW, 0.00005], + [self.plottingL, self.plottingBot, self.plottingW, 0.00005], ) timestampBar.axis('off') timestampBar.xaxis.set_minor_locator(AutoMinorLocator()) @@ -287,18 +314,19 @@ class PlottingWidget(QtWidgets.QScrollArea): 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) + if hasLabel: + timestampBar.set_ylabel('Hours', + fontweight='bold', + fontsize=self.fontSize, + rotation=0, + labelpad=self.labelPad, + ha='left', + color=self.displayColor['TM']) + + # self.update_timestamp_bar(timestampBar) return timestampBar - def __update_timestamp_bar(self, timestampBar): + def update_timestamp_bar(self, timestampBar): times, majorTimes, majorTimeLabels = getTimeTicks( self.currMinX, self.currMaxX, self.dateMode, self.timeTicksTotal) timestampBar.axis('on') @@ -309,7 +337,7 @@ class PlottingWidget(QtWidgets.QScrollArea): fontsize=self.fontSize+2) timestampBar.set_xlim(self.currMinX, self.currMaxX) - def __create_axes(self, plotB, plotH, hasMinMaxLines=True): + def create_axes(self, plotB, plotH, hasMinMaxLines=True): ax = self.canvas.figure.add_axes( [self.plottingL, plotB, self.plottingW, plotH], picker=True @@ -352,6 +380,7 @@ class PlottingWidget(QtWidgets.QScrollArea): size=self.fontSize ) titleVerAlignment = 'top' + if linkedAx is None: # set title on left side ax.text( @@ -414,14 +443,14 @@ class PlottingWidget(QtWidgets.QScrollArea): ax.spines['top'].set_visible(False) ax.spines['bottom'].set_visible(False) else: - minY = min(y) - maxY = max(y) + minY = y.min() + maxY = y.max() 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) + self.setAxesYlim(ax, minY, maxY) - def __setAxesYlim(self, ax, minY, maxY): + def setAxesYlim(self, ax, minY, maxY): minY = round(minY, 7) maxY = round(maxY, 7) if maxY > minY: @@ -443,10 +472,10 @@ class PlottingWidget(QtWidgets.QScrollArea): 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.plottingBot -= 0.003 + self.gapBar = self.create_axes(self.plottingBot, + 0.001, + hasMinMaxLines=False) self.updateGapBar() def updateGapBar(self): @@ -467,11 +496,11 @@ class PlottingWidget(QtWidgets.QScrollArea): lw=0., zorder=3)) # on top of center line - def __get_height(self, ratio): + def get_height(self, ratio): plotH = 0.0012 * ratio # ratio with figure height bwPlotsDistance = 0.0015 - self.plottingH -= plotH + bwPlotsDistance - self.plottingHPixel += 19 * ratio + self.plottingBot -= plotH + bwPlotsDistance + self.plottingBotPixel += 19 * ratio return plotH # -------------------- Different color dots ----------------------- # @@ -481,13 +510,13 @@ class PlottingWidget(QtWidgets.QScrollArea): """ plotH = 0.00001 bwPlotsDistance = 0.0001 - self.plottingH -= plotH + bwPlotsDistance - ax = self.__create_axes(self.plottingH, plotH, hasMinMaxLines=False) + self.plottingBot -= plotH + bwPlotsDistance + ax = self.create_axes(self.plottingBot, plotH, hasMinMaxLines=False) ax.x = None ax.plot([0], [0], linestyle="") return ax - def plotMultiColorDots(self, cData, chanDB, chan, linkedAx): + def plotMultiColorDots(self, cData, chanDB, chan, ax, linkedAx): """ plot scattered dots with colors defined by valueColors: *:W or -1:_|0:R|2.3:Y|+2.3:G @@ -498,12 +527,12 @@ class PlottingWidget(QtWidgets.QScrollArea): :return: """ - plotH = self.__get_height(chanDB['height']) - if linkedAx is None: - ax = self.__create_axes( - self.plottingH, plotH, hasMinMaxLines=False) - else: + plotH = self.get_height(chanDB['height']) + if linkedAx is not None: ax = linkedAx + if ax is None: + ax = self.create_axes( + self.plottingBot, plotH, hasMinMaxLines=False) x = [] prevVal = -constants.HIGHEST_INT @@ -519,15 +548,15 @@ class PlottingWidget(QtWidgets.QScrollArea): continue if v.startswith('+'): - points = [cData['decTimes'][i] - for i in range(len(cData['decData'])) - if cData['decData'][i] > val] + points = [cData['times'][i] + for i in range(len(cData['data'])) + if cData['data'][i] > val] elif v == '*': - points = cData['decTimes'] + points = cData['times'] else: - points = [cData['decTimes'][i] - for i in range(len(cData['decData'])) - if prevVal < cData['decData'][i] <= val] + points = [cData['times'][i] + for i in range(len(cData['data'])) + if prevVal < cData['data'][i] <= val] x += points ax.plot(points, len(points) * [0], linestyle="", @@ -554,16 +583,16 @@ class PlottingWidget(QtWidgets.QScrollArea): # return self.plotMultiColorDots(cData, chanDB, chan, linkedAx) # ---------------------------- up/down dots ---------------------------- # - def plotUpDownDots(self, cData, chanDB, chan, linkedAx): + def plotUpDownDots(self, cData, chanDB, chan, ax, 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: + plotH = self.get_height(chanDB['height']) + if linkedAx is not None: ax = linkedAx + if ax is None: + ax = self.create_axes( + self.plottingBot, plotH, hasMinMaxLines=False) valCols = chanDB['valueColors'].split('|') pointsList = [] @@ -572,9 +601,9 @@ class PlottingWidget(QtWidgets.QScrollArea): v, c = vc.split(':') val = getVal(v) - points = [cData['decTimes'][i] - for i in range(len(cData['decData'])) - if cData['decData'][i] == val] + points = [cData['times'][i] + for i in range(len(cData['data'])) + if cData['data'][i] == val] pointsList.append(points) colors.append(c) @@ -600,17 +629,17 @@ class PlottingWidget(QtWidgets.QScrollArea): 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: + def plotTimeDots(self, cData, chanDB, chan, ax, linkedAx): + plotH = self.get_height(chanDB['height']) + if linkedAx is not None: ax = linkedAx + if ax is None: + ax = self.create_axes(self.plottingBot, plotH) color = 'W' if chanDB['valueColors'] not in [None, 'None', '']: color = chanDB['valueColors'].strip() - x = cData['decTimes'] + x = cData['times'] self.setAxesInfo(ax, [len(x)], chanDB=chanDB, linkedAx=linkedAx) ax.myPlot = ax.plot(x, [0]*len(x), marker='s', markersize=1.5, @@ -624,15 +653,15 @@ class PlottingWidget(QtWidgets.QScrollArea): return ax # ----------------------- lines - one color dots ----------------------- # - def plotLinesDots(self, cData, chanDB, chan, linkedAx, info=''): + def plotLinesDots(self, cData, chanDB, chan, ax, linkedAx, info=''): """ L:G|D:W """ - plotH = self.__get_height(chanDB['height']) - if linkedAx is None: - ax = self.__create_axes(self.plottingH, plotH) - else: + plotH = self.get_height(chanDB['height']) + if linkedAx is not None: ax = linkedAx + if ax is None: + ax = self.create_axes(self.plottingBot, plotH) - x, y = cData['decTimes'], cData['decData'] + x, y = cData['times'], cData['data'] self.setAxesInfo(ax, [len(x)], chanDB=chanDB, info=info, y=y, linkedAx=linkedAx) colors = {} @@ -668,7 +697,7 @@ class PlottingWidget(QtWidgets.QScrollArea): ax.linkedY = y return ax - def plotLinesSRate(self, cData, chanDB, chan, linkedAx): + def plotLinesSRate(self, cData, chanDB, chan, ax, linkedAx): """ multi-line line seismic, one color, line only, can apply bit weights in (get_unit_bitweight()) @@ -677,21 +706,10 @@ class PlottingWidget(QtWidgets.QScrollArea): info = "%dsps" % cData['samplerate'] else: info = "%gsps" % cData['samplerate'] - return self.plotLinesDots(cData, chanDB, chan, linkedAx, info=info) + return self.plotLinesDots(cData, chanDB, chan, ax, 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) + def plotLinesMasspos(self, cData, chanDB, chan, ax, linkedAx): valueColors = getMassposValueColors( self.parent.massPosVoltRangeOpt, chan, self.cMode, self.errors, retType='tupleList') @@ -699,8 +717,8 @@ class PlottingWidget(QtWidgets.QScrollArea): if valueColors is None: return - plotH = self.__get_height(chanDB['height']) - ax = self.__create_axes(self.plottingH, plotH) + plotH = self.get_height(chanDB['height']) + ax = self.create_axes(self.plottingBot, plotH) ax.x, ax.y = cData['times'], cData['data'] self.setAxesInfo(ax, [len(ax.x)], chanDB=chanDB, y=ax.y) @@ -726,10 +744,9 @@ class PlottingWidget(QtWidgets.QScrollArea): count += 1 ax.scatter(ax.x, ax.y, marker='s', c=colors, s=sizes, zorder=3) return ax - # ---------------------------------------------------------# - def __add_ruler(self, color): + def add_ruler(self, color): ruler = ConnectionPatch( xyA=(0, 0), xyB=(0, self.bottom), @@ -743,9 +760,16 @@ class PlottingWidget(QtWidgets.QScrollArea): 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) + def set_lim(self, orgSize=False): + if not orgSize: + for chanID in self.plottingData1: + cData = self.plottingData1[chanID] + self.getZoomData(cData, chanID) + for chanID in self.plottingData2: + cData = self.plottingData2[chanID] + self.getZoomData(cData, chanID) + 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: @@ -781,9 +805,9 @@ class PlottingWidget(QtWidgets.QScrollArea): newY = ax.y[newMinXIndex:newMaxXIndex + 1] newMinY = min(newY) newMaxY = max(newY) - self.__setAxesYlim(ax, newMinY, newMaxY) + self.setAxesYlim(ax, newMinY, newMaxY) - def __set_title(self, title): + def set_title(self, title): self.fig.text(-0.15, 100, title, verticalalignment='top', horizontalalignment='left', @@ -791,7 +815,7 @@ class PlottingWidget(QtWidgets.QScrollArea): color=self.displayColor['TX'], size=self.fontSize) - def __draw(self): + def draw(self): try: self.canvas.draw() # a bug on mac: @@ -814,7 +838,7 @@ class PlottingWidget(QtWidgets.QScrollArea): def set_background_color(self, color='black'): self.fig.patch.set_facecolor(color) - self.__draw() + self.draw() def resetView(self): """ @@ -824,8 +848,8 @@ class PlottingWidget(QtWidgets.QScrollArea): return self.currMinX = self.minX self.currMaxX = self.maxX - self.__set_lim() - self.__draw() + self.set_lim() + self.draw() def clear(self): if self.zoomMarker1.get_visible(): @@ -834,30 +858,19 @@ class PlottingWidget(QtWidgets.QScrollArea): self.zoomMarker1.set_visible(True) # self.fig.clear() # self.axes = [] - self.__draw() + self.draw() - def decimateWConvertFactor(self, cData, convertFactor, maxDP=50000): + def applyConvertFactor(self, cData, convertFactor): """ 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): + # =============================== axes ================================ + def plot_channels(self, startTm, endTm, staID, trim_downsample, dataTime, + gaps, channelList, timeTicksTotal, + plottingData1, plottingData2): """ :param setID: (netID, statID, locID) :param plottingData: a ditionary including: @@ -867,83 +880,110 @@ class PlottingWidget(QtWidgets.QScrollArea): 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.trim_downsample = trim_downsample + self.plottingData1 = plottingData1 + self.plottingData2 = plottingData2 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.minX = self.currMinX = max(dataTime[0], startTm) + self.maxX = self.currMaxX = min(dataTime[1], endTm) + self.plotNo = len(self.plottingData1) + len(self.plottingData2) + title = getTitle(staID, self.minX, self.maxX, self.dateMode) + self.plottingBot = BOTTOM + self.plottingBotPixel = BOTTOM_PX + # self.plottingBot = 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()] + self.timestampBarTop = self.add_timestamp_bar(0.003) + self.set_title(title) + self.addGapBar(gaps) + notFoundChan = [c for c in channelList + if c not in self.plottingData1.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) + + for chanID in self.plottingData1: + print("chan soh ID:", chanID) + chanDB = extractData.getChanPlotInfo(chanID, 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.") + msg = (f"Channel {chanID}'s " + f"definition can't be found database.") displayTrackingInfo(self.parent, msg, 'warning') - plotType = chanDB['plotType'] if chanDB['plotType'] == '': continue + self.plottingData1[chanID]['chanDB'] = chanDB + self.getZoomData(self.plottingData1[chanID], chanID, True) + + for chanID in self.plottingData2: + print("masspos1 chanID:", chanID) + chanDB = extractData.getChanPlotInfo(chanID, self.parent.dataType) + self.plottingData2[chanID]['chanDB'] = chanDB + self.getZoomData(self.plottingData2[chanID], chanID, True) + + 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.plottingBotPixel: + self.widgt.setFixedHeight(self.plottingBotPixel) - cData = self.decimateWConvertFactor( - plottingData['channels'][chan], - chanDB['convertFactor'], - 50000) + self.draw() + + def getZoomData(self, cData, chanID, firsttime=False): + """ + :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)...] + """ + chanDB = cData['chanDB'] + plotType = chanDB['plotType'] + self.trim_downsample(cData, self.currMinX, self.currMaxX, firsttime) + self.applyConvertFactor(cData, 1) + if 'ax' not in cData: linkedAx = None if chanDB['linkedChan'] not in [None, 'None', '']: try: - linkedAx = plottingData['channels'][ + linkedAx = self.plottingData['channels'][ chanDB['linkedChan']]['ax'] except KeyError: pass - ax = getattr(self, - plotFunc[plotType][1])(cData, chanDB, chan, linkedAx) + ax = getattr(self, dbConf['plotFunc'][plotType][1])( + cData, chanDB, chanID, None, 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() + return + cData['ax'] = ax + ax.chan = chanID + self.axes.append(ax) + else: + getattr(self, dbConf['plotFunc'][plotType][1])( + cData, chanDB, chanID, cData['ax'], None) def hide_plots(self, plot_indexes): if self.axes == []: @@ -966,7 +1006,7 @@ class PlottingWidget(QtWidgets.QScrollArea): # currently consider every plot height are all 100px height height = self.widgt.geometry().height() - 100 * len(plot_indexes) self.widgt.setFixedHeight(height) - self.__draw() + self.draw() def hide_currplot(self): pos = self.currplot.get_position() @@ -982,7 +1022,7 @@ class PlottingWidget(QtWidgets.QScrollArea): height = self.widgt.geometry().height() - 100 self.widgt.setFixedHeight(height) self.hidden_plots[self.currplot_index] = h - self.__draw() + self.draw() def show_hidden_plot(self, index): h = self.hidden_plots[index] @@ -1000,7 +1040,7 @@ class PlottingWidget(QtWidgets.QScrollArea): self.widgt.setFixedHeight(height) del self.hidden_plots[index] pos = workplot.get_position() - self.__draw() + self.draw() def show_all_hidden_plots(self): plot_indexes = sorted(self.hidden_plots.keys()) @@ -1021,9 +1061,17 @@ class PlottingWidget(QtWidgets.QScrollArea): # currently consider every plot height are all 100px height height = self.widgt.geometry().height() + 100 * len(plot_indexes) self.widgt.setFixedHeight(height) - self.__draw() + self.draw() + + def rePlot(self): + for ax in self.axes: + print("pos", self.ax.get_position()) + # ============================= END AXES ============================== def set_colors(self, mode): self.cMode = mode self.displayColor = set_colors(mode) self.fig.patch.set_facecolor(self.displayColor['MF']) + + def setPeerPlottingWidgets(self, widgets): + self.peerPlottingWidgets = widgets diff --git a/sohstationviewer/view/mainwindow.py b/sohstationviewer/view/mainwindow.py index b7a90bbec32f2ba16f2ac2ae8cb1561edcf28b4b..ce3cbd60ef4f24019ad3cda4be8dc6dd1a94abbd 100755 --- a/sohstationviewer/view/mainwindow.py +++ b/sohstationviewer/view/mainwindow.py @@ -1,5 +1,7 @@ import pathlib import os +import re +from datetime import datetime from PySide2 import QtCore, QtWidgets @@ -13,6 +15,13 @@ 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 +from sohstationviewer.view.waveformdialog import WaveformDialog +from sohstationviewer.view.time_power_squareddialog import ( + TimePowerSquaredDialog) +from sohstationviewer.controller.util import displayTrackingInfo +from sohstationviewer.conf.constants import TM_FM +from sohstationviewer.model.handling_data import ( + trim_downsample_SOHChan, trim_downsample_WFChan) class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): @@ -29,9 +38,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): self.bitweightOpt = '' self.getChannelPrefer() self.YYYY_MM_DDAction.triggered.emit() + self.waveformDlg = WaveformDialog(self) + self.TPSDlg = TimePowerSquaredDialog(self) - def resizeEvent(self, event): - self.plottingWidget.init_size() + # def resizeEvent(self, event): + # self.plottingWidget.init_size() @QtCore.Slot() def openDataType(self): @@ -80,7 +91,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): self.currIDsNameLineEdit.setText('') @QtCore.Slot() - def replotLoadedData(self): + def resetView(self): self.plottingWidget.resetView() @QtCore.Slot() @@ -127,18 +138,18 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): @QtCore.Slot() def readSelectedFiles(self): + try: + del self.dataObject + except Exception: + pass 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 []) + self.reqInfoChans = (self.IDs if not self.allChanCheckBox.isChecked() + else []) # TODO: Having a form for user to create the list of channels to draw """ @@ -146,21 +157,17 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): It can be all chans in db or preference list of chans For Reftek, the list of channels is fixed => may not need """ - dirnames = [os.path.join(self.cwdLineEdit.text(), item.text()) - for item in self.openFilesList.selectedItems()] - if dirnames == []: + self.dirnames = [os.path.join(self.cwdLineEdit.text(), item.text()) + for item in self.openFilesList.selectedItems()] + if self.dirnames == []: msg = "No directories has been selected." QtWidgets.QMessageBox.warning(self, "Select directory", msg) return - self.dataType = detectDataType(self, dirnames) + self.dataType, _ = detectDataType(self, self.dirnames) if self.dataType is None: return - reqDSs = [] - for idx, DSCheckbox in enumerate(self.dsCheckBoxes): - if DSCheckbox.isChecked(): - reqDSs.append(idx + 1) - + # get reqInfoChans if (not self.allChanCheckBox.isChecked() and self.dataType != self.IDsDataType): msg = (f"DataType detected for the selected data set is " @@ -175,27 +182,101 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): return self.allChanCheckBox.setChecked(True) self.currIDsNameLineEdit.setText('') - reqInfoChans = [] + self.reqInfoChans = [] - plottingDataSets = loadData( - self.dataType, self, dirnames, reqInfoChans, reqDSs) + self.reqWFChans = [] + if self.dataType == 'RT130': + reqDSs = [] + for idx, DSCheckbox in enumerate(self.dsCheckBoxes): + if DSCheckbox.isChecked(): + reqDSs.append(idx + 1) + self.reqWFChans = reqDSs + else: + if self.wfAllCheckBox.isChecked(): + self.reqWFChans = ['*'] + elif self.wfChansLineEdit.text().strip() != "": + self.reqWFChans = self.wfChansLineEdit.text().split(",") + + startTmStr = self.timeFromDateEdit.date().toString(QtCore.Qt.ISODate) + endTmStr = self.timeToDateEdit.date().toString(QtCore.Qt.ISODate) + self.startTm = datetime.strptime(startTmStr, TM_FM).timestamp() + self.endTm = datetime.strptime(endTmStr, TM_FM).timestamp() + + self.dataObject = loadData(self.dataType, + self.trackingInfoTextBrowser, + self.dirnames, + reqWFChans=self.reqWFChans, + reqSOHChans=self.reqInfoChans, + readStart=self.startTm, + readEnd=self.endTm) + self.replotLoadedData() + + @QtCore.Slot() + def replotLoadedData(self): 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] + do = self.dataObject + + timeTickTotal = 5 # TODO: let user choose max ticks to be displayed + + selKey = do.selectedKey + + # do.createPlottingData(startTm, endTm) + # mainPlot + sohChans = (self.reqInfoChans if self.reqInfoChans != [] + else list(do.SOHData[selKey].keys())) + self.plottingWidget.plot_channels( + self.startTm, self.endTm, selKey, + trim_downsample_SOHChan, do.dataTime[selKey], + do.gaps[selKey], sohChans, timeTickTotal, + do.SOHData[selKey], do.massPosData[selKey]) + + peerPlottingWidgets = [self.plottingWidget] + + # waveformReqChans = (self.reqInfoChans + # if (self.reqInfoChans not in [[], ['SEISMIC']]) + # else list(plottingData['waveforms'].keys())) + # print("waveformReqChans:", waveformReqChans) + # + # if (self.TPSChansLineEdit.text().strip() != '' or + # self.TPSAllCheckBox.isChecked()): + # # TPSPlot + # if self.TPSAllCheckBox.isChecked(): + # TPSReqChans = waveformReqChans + # else: + # TPSReqChans = getReqTPSChans( + # self, waveformReqChans, + # self.TPSChansLineEdit.text().split(',')) + # + # peerPlottingWidgets.append(self.TPSDlg.plottingWidget) + # self.TPSDlg.setData(self.dataType, ','.join(self.dirnames)) + # self.TPSDlg.show() + # self.TPSDlg.plottingWidget.addPlots( + # setID, plottingData, TPSReqChans, timeTickTotal) + # else: + # self.TPSDlg.hide() + # + if self.reqWFChans != []: + # waveformPlot + peerPlottingWidgets.append(self.waveformDlg.plottingWidget) + self.waveformDlg.setData(self.dataType, ','.join(self.dirnames)) + self.waveformDlg.show() + wfChans = list(do.waveformData[do.selectedKey].keys()) + self.waveformDlg.plottingWidget.plot_channels( + self.startTm, self.endTm, selKey, + trim_downsample_WFChan, do.dataTime[selKey], + wfChans, timeTickTotal, + do.waveformData[selKey]['readData'], do.massPosData[selKey]) 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) + self.waveformDlg.hide() + + self.plottingWidget.setPeerPlottingWidgets(peerPlottingWidgets) + self.waveformDlg.plottingWidget.setPeerPlottingWidgets( + peerPlottingWidgets) + # self.TPSDlg.plottingWidget.setPeerPlottingWidsets( + # peerPlottingWidgets) def setCurrentDirectory(self, path=''): # Remove entries when cwd changes @@ -218,3 +299,18 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): self.IDsName = rows[0]['name'] self.IDs = [t.strip() for t in rows[0]['IDs'].split(',')] self.IDsDataType = rows[0]['dataType'] + + +def getReqTPSChans(parent, enteredTPSChans, waveformReqChans): + # TODO: unittest (may need to modify for better filter) + TPSReqChans = [] + for c in enteredTPSChans: + TPSRE = re.compile(c.strip().replace('*', '[ZNE123]')) + reqChans = [chan for chan in waveformReqChans + if TPSRE.match(chan)] + if reqChans == []: + msg = "%s doesn't match any seismic data channel." % c + displayTrackingInfo(parent, msg, 'warning') + else: + TPSReqChans += reqChans + return TPSReqChans diff --git a/sohstationviewer/view/paramdialog.py b/sohstationviewer/view/paramdialog.py index 9cdfcc2cf91ba7fa14f4c852194d23d23d6db72c..0315073a7ff1589cd15faaeaf81e1256d071c433 100755 --- a/sohstationviewer/view/paramdialog.py +++ b/sohstationviewer/view/paramdialog.py @@ -3,12 +3,15 @@ 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 + +from sohstationviewer.conf.dbSettings import dbConf class ParamDialog(Ui_DBInfoDialog): @@ -25,9 +28,9 @@ class ParamDialog(Ui_DBInfoDialog): 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]) + self.addWidget(self.dataList, rowidx, 4, range=[0, 10]) def getDataList(self): paramRows = executeDB('SELECT * FROM Parameters') @@ -42,7 +45,7 @@ class ParamDialog(Ui_DBInfoDialog): rowidx, 3).currentText().strip() valueColors = valueColorsString.split("|") for vc in valueColors: - if not conf['valColRE'].match(vc): + if not dbConf['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." diff --git a/sohstationviewer/view/selectbuttonsdialog.py b/sohstationviewer/view/selectbuttonsdialog.py new file mode 100644 index 0000000000000000000000000000000000000000..c5e2e82cf4171271b82d493d7ba1dc8dcfec6f12 --- /dev/null +++ b/sohstationviewer/view/selectbuttonsdialog.py @@ -0,0 +1,57 @@ +import sys +import platform +import os +from PySide2 import QtWidgets +from functools import partial + + +""" +Dialog includes buttons. +Click on a button will close the dialog and the index of the clicked button +can be retrieved in obj.ret +""" + + +class SelectButtonDialog(QtWidgets.QDialog): + def __init__(self, parent=None, message='', + buttonLabels=['test1', 'test2']): + super(SelectButtonDialog, self).__init__() + self.ret = -1 + height = 50 * (1 + len(buttonLabels) / 3) + self.setGeometry(100, 100, 900, height) + mainLayout = QtWidgets.QVBoxLayout() + self.setLayout(mainLayout) + + label = QtWidgets.QLabel(message) + mainLayout.addWidget(label, 0) + + buttonsLayout = QtWidgets.QGridLayout() + mainLayout.addLayout(buttonsLayout) + buttons = [] + r = -1 + for idx, label in enumerate(buttonLabels): + c = idx % 3 + if c == 0: + r += 1 + buttons.append(QtWidgets.QPushButton(label)) + buttons[idx].clicked.connect(partial(self.buttonClick, idx)) + buttonsLayout.addWidget(buttons[idx], r, c) + + def buttonClick(self, idx): + self.ret = idx + self.close() + + +if __name__ == '__main__': + os_name, version, *_ = platform.platform().split('-') + if os_name == 'macOS': + os.environ['QT_MAC_WANTS_LAYER'] = '1' + app = QtWidgets.QApplication(sys.argv) + + test = SelectButtonDialog(None, "Testing result from buttons", + ['test01', 'test02', 'test03', + 'test11', 'test12', 'test13']) + test.exec_() + + print("return Code:", test.ret) + sys.exit(app.exec_()) diff --git a/sohstationviewer/view/time_power_squareddialog.py b/sohstationviewer/view/time_power_squareddialog.py new file mode 100755 index 0000000000000000000000000000000000000000..277e29c6cfebcef714672957e7662720a43ccab2 --- /dev/null +++ b/sohstationviewer/view/time_power_squareddialog.py @@ -0,0 +1,145 @@ +# UI and connectSignals for MainWindow +import math + +from PySide2 import QtWidgets + +from sohstationviewer.view.core import plottingWidget as plottingWidget +from sohstationviewer.controller.plottingData import getTitle, getDayTicks + +SEC_PER_DAY = 86400 + + +class TimePowerSquaredWidget(plottingWidget.PlottingWidget): + + 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 + + """ + 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['earliestTmEpoch'] + self.maxX = self.currMaxX = plottingData['latestTmEpoch'] + self.plotNo = len(plottingData['waveforms']) + title = getTitle(self, setID, plottingData, self.dateMode) + self.plottingBot = plottingWidget.BOTTOM + self.plottingBotPixel = plottingWidget.BOTTOM_PX + self.axes = [] + self.timestampBarTop = self.add_timestamp_bar(0.003, hasLabel=False) + self.set_title(title) + print("reqInfoChans:", reqInfoChans) + for chan in reqInfoChans: + ax = self.plotTPS(plottingData['waveforms'][chan], chan) + if ax is None: + continue + # ax.chan = chan + 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.update_timestamp_bar(self.timestampBarTop) + # Set view size fit with the given data + if self.widgt.geometry().height() < self.plottingBotPixel: + self.widgt.setFixedHeight(self.plottingBotPixel) + + self.draw() + + def plotTPS(self, cData, chan): + print("plotTPS") + print("(self.maxX - self.minX)=", (self.maxX - self.minX)) + totalDays = 5 * math.ceil((self.maxX - self.minX) / SEC_PER_DAY) + print("totalDays:", totalDays) + plotH = self.get_height(totalDays) + ax = self.create_axes(self.plottingBot, plotH) + return ax + + def create_axes(self, plotB, plotH, hasMinMaxLines=False): + ax = self.canvas.figure.add_axes( + [self.plottingL, plotB, self.plottingW, plotH], + picker=True + ) + # ax.axis('off') + ax.spines['right'].set_visible(False) + ax.spines['left'].set_visible(False) + ax.xaxis.grid(True, which='major', color='r', linestyle='-') + ax.xaxis.grid(True, which='minor', color='b', linestyle='--') + ax.set_yticks([]) + ax.tick_params(which='major', length=7, width=2, + direction='inout', + colors=self.displayColor['TM'], + labelbottom=True, + labeltop=False) + ax.tick_params(which='minor', length=4, width=1, + direction='inout', + colors=self.displayColor['TM']) + + times, majorTimes, majorTimeLabels = getDayTicks() + ax.set_xticks(times, minor=True) + ax.set_xticks(majorTimes) + ax.set_xticklabels(majorTimeLabels, fontsize=self.fontSize) + ax.set_xlim(1, 24) + ax.patch.set_alpha(0) + return ax + + def update_timestamp_bar(self, timestampBar): + times, majorTimes, majorTimeLabels = getDayTicks() + timestampBar.axis('on') + timestampBar.set_yticks([]) + timestampBar.set_xticks(times, minor=True) + timestampBar.set_xticks(majorTimes) + timestampBar.set_xticklabels(majorTimeLabels, + fontsize=self.fontSize) + timestampBar.set_xlim(1, 24) + + +class TimePowerSquaredDialog(QtWidgets.QWidget): + def __init__(self, parent): + super().__init__() + self.parent = parent + self.dateFormat = self.parent.dateFormat + self.bitweightOpt = self.parent.bitweightOpt + self.resize(1000, 700) + self.setWindowTitle("TPS Plot") + + mainLayout = QtWidgets.QVBoxLayout() + self.setLayout(mainLayout) + mainLayout.setContentsMargins(5, 5, 5, 5) + mainLayout.setSpacing(0) + + self.plottingWidget = TimePowerSquaredWidget( + self, "timepowersquaredwidget") + mainLayout.addWidget(self.plottingWidget, 2) + + bottomLayout = QtWidgets.QHBoxLayout() + mainLayout.addLayout(bottomLayout) + + self.writePSButton = QtWidgets.QPushButton('Write .ps', self) + bottomLayout.addWidget(self.writePSButton) + self.trackingInfoTextBrowser = QtWidgets.QTextBrowser(self) + self.trackingInfoTextBrowser.setFixedHeight(60) + bottomLayout.addWidget(self.trackingInfoTextBrowser) + + self.connectSignals() + + def setData(self, dataType, fileName): + self.dataType = dataType + self.setWindowTitle("TPS Plot %s - %s" % (dataType, fileName)) + + def resizeEvent(self, event): + self.plottingWidget.init_size() + + def connectSignals(self): + print("connectSignals") diff --git a/sohstationviewer/view/ui/main.ui b/sohstationviewer/view/ui/main.ui deleted file mode 100755 index 117fd8eb7a8fd2f0f5354707e6160f2f2ce0f83e..0000000000000000000000000000000000000000 --- a/sohstationviewer/view/ui/main.ui +++ /dev/null @@ -1,1357 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>MainWindow</class> - <widget class="QMainWindow" name="MainWindow"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>1798</width> - <height>1110</height> - </rect> - </property> - <property name="windowTitle"> - <string>MainWindow</string> - </property> - <property name="unifiedTitleAndToolBarOnMac"> - <bool>false</bool> - </property> - <widget class="QWidget" name="centralwidget"> - <layout class="QGridLayout" name="gridLayout"> - <property name="leftMargin"> - <number>2</number> - </property> - <property name="topMargin"> - <number>2</number> - </property> - <property name="rightMargin"> - <number>2</number> - </property> - <property name="bottomMargin"> - <number>2</number> - </property> - <item row="0" column="0"> - <widget class="QPushButton" name="cwdPushButton"> - <property name="text"> - <string>Main Data Directory</string> - </property> - </widget> - </item> - <item row="0" column="1"> - <widget class="QLineEdit" name="cwdLineEdit"/> - </item> - <item row="0" column="2"> - <spacer name="horizontalSpacer"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeType"> - <enum>QSizePolicy::Preferred</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item row="0" column="3"> - <layout class="QGridLayout" name="timeFromGrid"> - <item row="0" column="0"> - <widget class="QLabel" name="timeFromLabel"> - <property name="text"> - <string>From</string> - </property> - <property name="buddy"> - <cstring>timeFromDateEdit</cstring> - </property> - </widget> - </item> - <item row="0" column="1"> - <widget class="QDateEdit" name="timeFromDateEdit"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="displayFormat"> - <string>yyyy-MM-dd</string> - </property> - <property name="calendarPopup"> - <bool>true</bool> - </property> - </widget> - </item> - </layout> - </item> - <item row="0" column="4"> - <layout class="QGridLayout" name="timeToGrid"> - <item row="0" column="0"> - <widget class="QLabel" name="timeToLabel"> - <property name="text"> - <string>To</string> - </property> - <property name="buddy"> - <cstring>timeToDateEdit</cstring> - </property> - </widget> - </item> - <item row="0" column="1"> - <widget class="QDateEdit" name="timeToDateEdit"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="displayFormat"> - <string>yyyy-MM-dd</string> - </property> - <property name="calendarPopup"> - <bool>true</bool> - </property> - </widget> - </item> - </layout> - </item> - <item row="1" column="0" colspan="5"> - <widget class="QSplitter" name="verticalSplit"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="handleWidth"> - <number>2</number> - </property> - <property name="childrenCollapsible"> - <bool>false</bool> - </property> - <widget class="QWidget" name="mainWidget" native="true"> - <layout class="QGridLayout" name="gridLayout_3"> - <property name="leftMargin"> - <number>0</number> - </property> - <property name="topMargin"> - <number>0</number> - </property> - <property name="rightMargin"> - <number>0</number> - </property> - <property name="bottomMargin"> - <number>0</number> - </property> - <item row="0" column="0"> - <widget class="QSplitter" name="horizontalSplit"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="handleWidth"> - <number>2</number> - </property> - <property name="childrenCollapsible"> - <bool>false</bool> - </property> - <widget class="QScrollArea" name="sideScrollArea"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Expanding"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="minimumSize"> - <size> - <width>0</width> - <height>0</height> - </size> - </property> - <property name="maximumSize"> - <size> - <width>250</width> - <height>16777215</height> - </size> - </property> - <property name="widgetResizable"> - <bool>true</bool> - </property> - <widget class="QWidget" name="sideScrollAreaContents"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>264</width> - <height>814</height> - </rect> - </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <property name="spacing"> - <number>2</number> - </property> - <property name="leftMargin"> - <number>0</number> - </property> - <property name="topMargin"> - <number>0</number> - </property> - <property name="rightMargin"> - <number>0</number> - </property> - <property name="bottomMargin"> - <number>0</number> - </property> - <item> - <layout class="QGridLayout" name="primaryGrid"> - <item row="24" column="1"> - <layout class="QGridLayout" name="controlButtonGrid"> - <property name="spacing"> - <number>2</number> - </property> - <item row="0" column="3"> - <widget class="QPushButton" name="writePushButton"> - <property name="text"> - <string>Write .ps</string> - </property> - </widget> - </item> - <item row="0" column="2"> - <widget class="QPushButton" name="stopPushButton"> - <property name="text"> - <string>Stop</string> - </property> - </widget> - </item> - <item row="0" column="1"> - <widget class="QPushButton" name="readPushButton"> - <property name="text"> - <string>Read</string> - </property> - </widget> - </item> - <item row="0" column="0"> - <spacer name="horizontalSpacer_7"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item row="0" column="4"> - <spacer name="horizontalSpacer_8"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - </layout> - </item> - <item row="26" column="0" colspan="3"> - <widget class="QListWidget" name="listWidget_2"/> - </item> - <item row="7" column="1"> - <widget class="Line" name="sep2"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - </widget> - </item> - <item row="22" column="1"> - <layout class="QGridLayout" name="dsGrid"> - <item row="2" column="3"> - <widget class="QCheckBox" name="ds6CheckBox"> - <property name="text"> - <string>6</string> - </property> - </widget> - </item> - <item row="0" column="3"> - <widget class="QCheckBox" name="ds3CheckBox"> - <property name="text"> - <string>3</string> - </property> - </widget> - </item> - <item row="3" column="2"> - <widget class="QCheckBox" name="dsTPSCheckBox"> - <property name="text"> - <string>TPS</string> - </property> - </widget> - </item> - <item row="2" column="0"> - <widget class="QLabel" name="dsLabel"> - <property name="text"> - <string>DSs:</string> - </property> - <property name="buddy"> - <cstring>ds1CheckBox</cstring> - </property> - </widget> - </item> - <item row="2" column="4"> - <spacer name="horizontalSpacer_6"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item row="0" column="2"> - <widget class="QCheckBox" name="ds2CheckBox"> - <property name="text"> - <string>2</string> - </property> - </widget> - </item> - <item row="0" column="1"> - <widget class="QCheckBox" name="ds1CheckBox"> - <property name="text"> - <string>1</string> - </property> - </widget> - </item> - <item row="2" column="1"> - <widget class="QCheckBox" name="ds4CheckBox"> - <property name="text"> - <string>4</string> - </property> - </widget> - </item> - <item row="2" column="2"> - <widget class="QCheckBox" name="ds5checkBox"> - <property name="text"> - <string>5</string> - </property> - </widget> - </item> - </layout> - </item> - <item row="20" column="1"> - <layout class="QGridLayout" name="massPosGrid"> - <item row="0" column="0"> - <widget class="QLabel" name="massPosLabel"> - <property name="text"> - <string>Mass Pos:</string> - </property> - <property name="buddy"> - <cstring>lowChanCheckBox</cstring> - </property> - </widget> - </item> - <item row="0" column="2"> - <widget class="QCheckBox" name="hiChanCheckbox"> - <property name="text"> - <string>456</string> - </property> - </widget> - </item> - <item row="0" column="1"> - <widget class="QCheckBox" name="lowChanCheckBox"> - <property name="text"> - <string>123</string> - </property> - </widget> - </item> - <item row="0" column="3"> - <spacer name="horizontalSpacer_3"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - </layout> - </item> - <item row="12" column="1"> - <layout class="QGridLayout" name="zoomGrid"> - <property name="leftMargin"> - <number>0</number> - </property> - <item row="0" column="1"> - <widget class="QSpinBox" name="horizontalZoomSpinBox"/> - </item> - <item row="0" column="0"> - <widget class="QLabel" name="horizontalZoomLabel"> - <property name="text"> - <string>Mag. X:</string> - </property> - <property name="buddy"> - <cstring>horizontalZoomSpinBox</cstring> - </property> - </widget> - </item> - <item row="1" column="1"> - <widget class="QSpinBox" name="verticalZoomSpinBox"/> - </item> - <item row="1" column="0"> - <widget class="QLabel" name="verticalZoomLabel"> - <property name="text"> - <string>Mag. Y:</string> - </property> - <property name="buddy"> - <cstring>verticalZoomSpinBox</cstring> - </property> - </widget> - </item> - <item row="0" column="2"> - <spacer name="horizontalSpacer_2"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - </layout> - </item> - <item row="4" column="0" colspan="2"> - <layout class="QGridLayout" name="mainOptionsGrid"> - <property name="horizontalSpacing"> - <number>1</number> - </property> - <property name="verticalSpacing"> - <number>2</number> - </property> - <item row="2" column="0"> - <widget class="QLineEdit" name="searchLineEdit"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="minimumSize"> - <size> - <width>100</width> - <height>0</height> - </size> - </property> - <property name="maximumSize"> - <size> - <width>150</width> - <height>16777215</height> - </size> - </property> - <property name="placeholderText"> - <string>Search...</string> - </property> - </widget> - </item> - <item row="3" column="0"> - <layout class="QGridLayout" name="fileListGrid"> - <property name="spacing"> - <number>2</number> - </property> - <item row="0" column="1"> - <widget class="QCheckBox" name="fileListLogCheckBox"> - <property name="text"> - <string>.log</string> - </property> - </widget> - </item> - <item row="1" column="2"> - <widget class="QCheckBox" name="fileListZipCheckBox"> - <property name="text"> - <string>.zip</string> - </property> - </widget> - </item> - <item row="1" column="1"> - <widget class="QCheckBox" name="fileListCfCheckBox"> - <property name="text"> - <string>.cf</string> - </property> - </widget> - </item> - <item row="0" column="2"> - <widget class="QCheckBox" name="fileListRefCheckBox"> - <property name="text"> - <string>.ref</string> - </property> - </widget> - </item> - <item row="0" column="0"> - <widget class="QLabel" name="label_4"> - <property name="text"> - <string>List:</string> - </property> - <property name="buddy"> - <cstring>fileListLogCheckBox</cstring> - </property> - </widget> - </item> - <item row="0" column="5"> - <spacer name="horizontalSpacer_5"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - </layout> - </item> - <item row="4" column="0"> - <layout class="QGridLayout" name="backgroundGrid"> - <property name="spacing"> - <number>2</number> - </property> - <item row="0" column="1"> - <widget class="QRadioButton" name="backgroundWhiteRadioButton"> - <property name="text"> - <string>B</string> - </property> - <property name="checked"> - <bool>true</bool> - </property> - <attribute name="buttonGroup"> - <string notr="true">buttonGroup</string> - </attribute> - </widget> - </item> - <item row="0" column="2"> - <widget class="QRadioButton" name="backgroundBlackRadioButton"> - <property name="text"> - <string>W</string> - </property> - <attribute name="buttonGroup"> - <string notr="true">buttonGroup</string> - </attribute> - </widget> - </item> - <item row="0" column="0"> - <widget class="QLabel" name="backgroundLabel"> - <property name="text"> - <string>Background:</string> - </property> - <property name="buddy"> - <cstring>backgroundWhiteRadioButton</cstring> - </property> - </widget> - </item> - <item row="0" column="3"> - <spacer name="horizontalSpacer_4"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - </layout> - </item> - <item row="2" column="1" rowspan="2"> - <layout class="QGridLayout" name="plotControlGrid"> - <property name="spacing"> - <number>2</number> - </property> - <item row="0" column="0"> - <widget class="QPushButton" name="clearPushButton"> - <property name="text"> - <string>Clear</string> - </property> - </widget> - </item> - <item row="1" column="0"> - <widget class="QPushButton" name="replotPushButton"> - <property name="text"> - <string>Replot</string> - </property> - </widget> - </item> - <item row="2" column="0"> - <widget class="QPushButton" name="reloadPushButton"> - <property name="text"> - <string>Reload</string> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </item> - <item row="8" column="1"> - <widget class="QCheckBox" name="sohCheckBox"> - <property name="text"> - <string>SOH Only</string> - </property> - </widget> - </item> - <item row="23" column="1"> - <widget class="Line" name="sep0"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - </widget> - </item> - <item row="2" column="0" colspan="2"> - <widget class="QListWidget" name="openFilesList"/> - </item> - <item row="21" column="1"> - <widget class="Line" name="sep1"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - </widget> - <widget class="QScrollArea" name="mainScrollArea"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="widgetResizable"> - <bool>true</bool> - </property> - <widget class="QWidget" name="mainScrollAreaContents"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>1540</width> - <height>828</height> - </rect> - </property> - <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> - <horstretch>5</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <layout class="QGridLayout" name="gridLayout_2"> - <property name="leftMargin"> - <number>0</number> - </property> - <property name="topMargin"> - <number>0</number> - </property> - <property name="rightMargin"> - <number>0</number> - </property> - <property name="bottomMargin"> - <number>0</number> - </property> - <property name="spacing"> - <number>2</number> - </property> - <item row="0" column="0"> - <widget class="PlottingWidget" name="plottingWidget"/> - </item> - </layout> - </widget> - </widget> - </widget> - </item> - </layout> - </widget> - <widget class="QTextBrowser" name="trackingInfoTextBrowser"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Maximum"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="maximumSize"> - <size> - <width>16777215</width> - <height>200</height> - </size> - </property> - </widget> - </widget> - </item> - </layout> - </widget> - <widget class="QMenuBar" name="menubar"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>1798</width> - <height>20</height> - </rect> - </property> - <widget class="QMenu" name="menu_File"> - <property name="title"> - <string>&File</string> - </property> - <addaction name="deleteSetup"/> - <addaction name="separator"/> - <addaction name="quit"/> - </widget> - <widget class="QMenu" name="menu_Command"> - <property name="title"> - <string>&Commands</string> - </property> - <widget class="QMenu" name="menu_Export_TT_Times_As"> - <property name="title"> - <string>&Export TT Times As...</string> - </property> - <addaction name="exportDeploymentFile"/> - <addaction name="exportDeploymentBlock"/> - <addaction name="exportTSPGeometry"/> - <addaction name="exportShotInfo"/> - </widget> - <addaction name="openGPSPlots"/> - <addaction name="searchLog"/> - <addaction name="plotTimeRanges"/> - <addaction name="plotPositions"/> - <addaction name="separator"/> - <addaction name="menu_Export_TT_Times_As"/> - </widget> - <widget class="QMenu" name="menu_Options"> - <property name="title"> - <string>&Options</string> - </property> - <addaction name="plotTimingProblems"/> - <addaction name="filterNonSOHLines"/> - <addaction name="separator"/> - <addaction name="sortFilesByType"/> - <addaction name="sortFilesAlphabetically"/> - <addaction name="calculateFileSizes"/> - <addaction name="warnIfBig"/> - <addaction name="separator"/> - <addaction name="addMassPositionToSOH"/> - <addaction name="colorMPRegular"/> - <addaction name="colorMPTrillium"/> - <addaction name="separator"/> - <addaction name="addPositionsToET"/> - <addaction name="separator"/> - <addaction name="readAntellopeLog"/> - <addaction name="separator"/> - <addaction name="showYYYYDOYDates"/> - <addaction name="showYYYY_MM_DDDates"/> - <addaction name="showYYYYMMMDDDates"/> - <addaction name="separator"/> - <addaction name="setFontSizes"/> - </widget> - <widget class="QMenu" name="menu_Help"> - <property name="title"> - <string>&Help</string> - </property> - <addaction name="openCalendar"/> - <addaction name="openAbout"/> - </widget> - <widget class="QMenu" name="menuForm"> - <property name="title"> - <string>F&orms</string> - </property> - </widget> - <widget class="QMenu" name="menuPlots"> - <property name="title"> - <string>&Plots</string> - </property> - <addaction name="plotDSPClkDifference"/> - <addaction name="plotPhaseError"/> - <addaction name="plotJerk"/> - <addaction name="plotFileErrors"/> - <addaction name="plotGPSOnOffErr"/> - <addaction name="plotGPSLkUnlk"/> - <addaction name="plotTemperature"/> - <addaction name="plotVoltage"/> - <addaction name="plotBackupVoltage"/> - <addaction name="plotDumpCall"/> - <addaction name="plotAcquisitionOnOff"/> - <addaction name="plotResetPowerUp"/> - <addaction name="plotErrorWarning"/> - <addaction name="plotDescrepancies"/> - <addaction name="plotSOHDataDefinitions"/> - <addaction name="plotNetworkUpDown"/> - <addaction name="plotEvents"/> - <addaction name="plotDisk1Usage"/> - <addaction name="plotDisk2Usage"/> - <addaction name="plotMassPositions123"/> - <addaction name="plotMassPositions456"/> - <addaction name="separator"/> - <addaction name="plotAll"/> - </widget> - <addaction name="menu_File"/> - <addaction name="menu_Command"/> - <addaction name="menuPlots"/> - <addaction name="menu_Options"/> - <addaction name="menuForm"/> - <addaction name="menu_Help"/> - </widget> - <widget class="QStatusBar" name="statusbar"/> - <action name="quit"> - <property name="text"> - <string>&Quit</string> - </property> - </action> - <action name="deleteSetup"> - <property name="text"> - <string>&Delete Setup File</string> - </property> - </action> - <action name="openGPSPlots"> - <property name="text"> - <string>&GPS Plotter</string> - </property> - </action> - <action name="searchLog"> - <property name="text"> - <string>Log &Search</string> - </property> - </action> - <action name="plotTimeRanges"> - <property name="text"> - <string>Plot &Time Ranges</string> - </property> - </action> - <action name="plotPositions"> - <property name="text"> - <string>Plot &Positions</string> - </property> - </action> - <action name="exportDeploymentFile"> - <property name="text"> - <string>Deployment File (&Line)</string> - </property> - </action> - <action name="exportDeploymentBlock"> - <property name="text"> - <string>Deployment File (&Block)</string> - </property> - </action> - <action name="exportTSPGeometry"> - <property name="text"> - <string>&TSP Shotfile / Geometry</string> - </property> - </action> - <action name="exportShotInfo"> - <property name="text"> - <string>&Shot Info</string> - </property> - </action> - <action name="openCalendar"> - <property name="text"> - <string>&Calendar</string> - </property> - </action> - <action name="plotDSPClkDifference"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>DSP-Clk Difference</string> - </property> - </action> - <action name="plotPhaseError"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Phase Error</string> - </property> - </action> - <action name="plotJerk"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Jerk/DSP Sets</string> - </property> - </action> - <action name="plotFileErrors"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>.err File Errors</string> - </property> - </action> - <action name="plotGPSOnOffErr"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>GPS On/Off/Err</string> - </property> - </action> - <action name="plotGPSLkUnlk"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>GPS Lk-Unlk</string> - </property> - </action> - <action name="plotTemperature"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Temperature</string> - </property> - </action> - <action name="plotVoltage"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Volts</string> - </property> - </action> - <action name="plotBackupVoltage"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Backup Volts</string> - </property> - </action> - <action name="plotDumpCall"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Dump Call</string> - </property> - </action> - <action name="plotAcquisitionOnOff"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Acquisition On/Off</string> - </property> - </action> - <action name="plotResetPowerUp"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Reset/Powerup</string> - </property> - </action> - <action name="plotErrorWarning"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Error/Warning</string> - </property> - </action> - <action name="plotDescrepancies"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Discrepancies</string> - </property> - </action> - <action name="plotSOHDataDefinitions"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>SOH/Data Definitions</string> - </property> - </action> - <action name="plotNetworkUpDown"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Network Up/Down</string> - </property> - </action> - <action name="plotEvents"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Events</string> - </property> - </action> - <action name="plotDisk1Usage"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Disk 1 Usage</string> - </property> - </action> - <action name="plotDisk2Usage"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Disk 2 Usage</string> - </property> - </action> - <action name="plotMassPositions123"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Mass Positions 123</string> - </property> - </action> - <action name="plotMassPositions456"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Mass Positions 456</string> - </property> - </action> - <action name="plotAll"> - <property name="text"> - <string>All Plots</string> - </property> - </action> - <action name="plotTimingProblems"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Plot Timing Problems</string> - </property> - </action> - <action name="filterNonSOHLines"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Filter Non-SOH Lines</string> - </property> - </action> - <action name="sortFilesByType"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Sort Files List By Type</string> - </property> - </action> - <action name="sortFilesAlphabetically"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Sort Files List Alphabetically</string> - </property> - </action> - <action name="calculateFileSizes"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Calculate File Sizes</string> - </property> - </action> - <action name="warnIfBig"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Warn If Big</string> - </property> - </action> - <action name="addMassPositionToSOH"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Add Mass Positions to SOH Messages</string> - </property> - </action> - <action name="colorMPRegular"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>MP Coloring (Regular)</string> - </property> - </action> - <action name="colorMPTrillium"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>MP Coloring (Trillium)</string> - </property> - </action> - <action name="addPositionsToET"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Add Positions to ET Lines</string> - </property> - </action> - <action name="readAntellopeLog"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Read Antelope-Produced Log File</string> - </property> - </action> - <action name="showYYYYDOYDates"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Show YYYY:DOY Dates</string> - </property> - </action> - <action name="showYYYY_MM_DDDates"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Show YYYY-MM-DD Dates</string> - </property> - </action> - <action name="showYYYYMMMDDDates"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Show YYYYMMMDD Dates</string> - </property> - </action> - <action name="setFontSizes"> - <property name="text"> - <string>Set Font Sizes</string> - </property> - </action> - <action name="openAbout"> - <property name="text"> - <string>&About</string> - </property> - </action> - </widget> - <customwidgets> - <customwidget> - <class>PlottingWidget</class> - <extends>QGraphicsView</extends> - <header>plottingwidget.h</header> - <slots> - <slot>replotLoadedData()</slot> - </slots> - </customwidget> - </customwidgets> - <resources/> - <connections> - <connection> - <sender>clearPushButton</sender> - <signal>clicked()</signal> - <receiver>searchLineEdit</receiver> - <slot>clear()</slot> - <hints> - <hint type="sourcelabel"> - <x>197</x> - <y>193</y> - </hint> - <hint type="destinationlabel"> - <x>137</x> - <y>191</y> - </hint> - </hints> - </connection> - <connection> - <sender>quit</sender> - <signal>triggered(bool)</signal> - <receiver>MainWindow</receiver> - <slot>close()</slot> - <hints> - <hint type="sourcelabel"> - <x>-1</x> - <y>-1</y> - </hint> - <hint type="destinationlabel"> - <x>506</x> - <y>299</y> - </hint> - </hints> - </connection> - <connection> - <sender>readPushButton</sender> - <signal>clicked()</signal> - <receiver>MainWindow</receiver> - <slot>readSelectedFile()</slot> - <hints> - <hint type="sourcelabel"> - <x>68</x> - <y>421</y> - </hint> - <hint type="destinationlabel"> - <x>415</x> - <y>-8</y> - </hint> - </hints> - </connection> - <connection> - <sender>stopPushButton</sender> - <signal>clicked()</signal> - <receiver>MainWindow</receiver> - <slot>stopFileRead()</slot> - <hints> - <hint type="sourcelabel"> - <x>145</x> - <y>428</y> - </hint> - <hint type="destinationlabel"> - <x>325</x> - <y>-12</y> - </hint> - </hints> - </connection> - <connection> - <sender>writePushButton</sender> - <signal>clicked()</signal> - <receiver>MainWindow</receiver> - <slot>writePSFile()</slot> - <hints> - <hint type="sourcelabel"> - <x>190</x> - <y>423</y> - </hint> - <hint type="destinationlabel"> - <x>346</x> - <y>-28</y> - </hint> - </hints> - </connection> - <connection> - <sender>replotPushButton</sender> - <signal>clicked()</signal> - <receiver>plottingWidget</receiver> - <slot>replotLoadedData()</slot> - <hints> - <hint type="sourcelabel"> - <x>227</x> - <y>221</y> - </hint> - <hint type="destinationlabel"> - <x>603</x> - <y>215</y> - </hint> - </hints> - </connection> - <connection> - <sender>reloadPushButton</sender> - <signal>clicked()</signal> - <receiver>MainWindow</receiver> - <slot>reloadFile()</slot> - <hints> - <hint type="sourcelabel"> - <x>222</x> - <y>233</y> - </hint> - <hint type="destinationlabel"> - <x>510</x> - <y>-11</y> - </hint> - </hints> - </connection> - <connection> - <sender>cwdPushButton</sender> - <signal>clicked()</signal> - <receiver>MainWindow</receiver> - <slot>changeCurrentDirectory()</slot> - <hints> - <hint type="sourcelabel"> - <x>49</x> - <y>31</y> - </hint> - <hint type="destinationlabel"> - <x>144</x> - <y>-24</y> - </hint> - </hints> - </connection> - <connection> - <sender>MainWindow</sender> - <signal>currentDirectoryChanged(QString)</signal> - <receiver>cwdLineEdit</receiver> - <slot>setText(QString)</slot> - <hints> - <hint type="sourcelabel"> - <x>2</x> - <y>20</y> - </hint> - <hint type="destinationlabel"> - <x>272</x> - <y>28</y> - </hint> - </hints> - </connection> - </connections> - <slots> - <signal>currentDirectoryChanged(QString)</signal> - <slot>readSelectedFile()</slot> - <slot>stopFileRead()</slot> - <slot>writePSFile()</slot> - <slot>reloadFile()</slot> - <slot>changeCurrentDirectory(QString)</slot> - </slots> - <buttongroups> - <buttongroup name="buttonGroup"/> - </buttongroups> -</ui> diff --git a/sohstationviewer/view/ui/main_ui.py b/sohstationviewer/view/ui/main_ui.py index aecc4a4d09575a99021c93b4aa6b7bf7a4e98b8c..1c9a7f69334cfcbac86c637a538981db632ba3f5 100755 --- a/sohstationviewer/view/ui/main_ui.py +++ b/sohstationviewer/view/ui/main_ui.py @@ -3,7 +3,9 @@ from PySide2 import QtCore, QtGui, QtWidgets from sohstationviewer.view.core.calendarwidget import CalendarWidget + from sohstationviewer.view.core.plottingWidget import PlottingWidget +from sohstationviewer.conf import constants class Ui_MainWindow(object): @@ -65,8 +67,8 @@ class Ui_MainWindow(object): self.setControlColumn(hLayout) - self.plottingWidget = PlottingWidget( - self.MainWindow) + self.plottingWidget = PlottingWidget(self.MainWindow, 'SOHWidget') + hLayout.addWidget(self.plottingWidget, 2) def setControlColumn(self, parentLayout): @@ -117,7 +119,9 @@ class Ui_MainWindow(object): searchGrid.addWidget(self.fileListZipCheckBox, 1, 2, 1, 1) self.replotButton = QtWidgets.QPushButton('RePlot', self.centralWidget) - self.replotButton.setFixedWidth(65) + + self.replotButton.setFixedWidth(95) + searchGrid.addWidget(self.replotButton, 1, 3, 1, 1) backgroundLayout = QtWidgets.QHBoxLayout() @@ -133,30 +137,17 @@ class Ui_MainWindow(object): 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) + gapLayout = QtWidgets.QHBoxLayout() + leftLayout.addLayout(gapLayout) self.detectGapCheckBox = QtWidgets.QCheckBox( 'DetectGap Len:', self.centralWidget) - TPS_gap_SOH_Layout.addWidget(self.detectGapCheckBox, 1, 0, 1, 2) + gapLayout.addWidget(self.detectGapCheckBox) - # 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) + gapLayout.addWidget(self.gapLenLineEdit) - self.sohCheckBox = QtWidgets.QCheckBox('SOH Only', self.centralWidget) - TPS_gap_SOH_Layout.addWidget(self.sohCheckBox, 2, 0) + gapLayout.addWidget(QtWidgets.QLabel('m')) massPosLayout = QtWidgets.QHBoxLayout() # massPosLayout.setContentsMargins(0, 0, 0, 0) @@ -186,6 +177,17 @@ class Ui_MainWindow(object): QtWidgets.QCheckBox('%s' % count, self.centralWidget)) dsGrid.addWidget(self.dsCheckBoxes[count - 1], r, c + 1) + wfGrid = QtWidgets.QGridLayout() + leftLayout.addLayout(wfGrid) + self.wfAllCheckBox = QtWidgets.QCheckBox( + 'All WFChans:', self.centralWidget) + wfGrid.addWidget(self.wfAllCheckBox, 0, 0, 1, 2) + + self.wfChansLineEdit = QtWidgets.QLineEdit(self.centralWidget) + wfGrid.addWidget(self.wfChansLineEdit, 0, 3, 1, 2) + + self.tpsCheckBox = QtWidgets.QCheckBox('TPS', self.centralWidget) + wfGrid.addWidget(self.tpsCheckBox, 1, 0) self.addSeperationLine(leftLayout) chanLayout = QtWidgets.QHBoxLayout() @@ -443,12 +445,16 @@ class Ui_MainWindow(object): self.timeToDateEdit.setDate(QtCore.QDate.currentDate()) self.timeFromDateEdit.setCalendarWidget(CalendarWidget(MainWindow)) - self.timeFromDateEdit.setDate(QtCore.QDate.currentDate()) + self.timeFromDateEdit.setDate(QtCore.QDate.fromString( + constants.DEFAULT_START_TIME, QtCore.Qt.ISODate + )) # second Row self.openFilesList.itemDoubleClicked.connect( MainWindow.openFilesListItemDoubleClicked) + # self.resetButton.clicked.connect(MainWindow.resetView) + self.replotButton.clicked.connect(MainWindow.replotLoadedData) self.allChanCheckBox.clicked.connect( diff --git a/sohstationviewer/view/waveformdialog.py b/sohstationviewer/view/waveformdialog.py new file mode 100755 index 0000000000000000000000000000000000000000..75b10bd9ce8ca93c5ffc51b0781eec8dbd9a50f1 --- /dev/null +++ b/sohstationviewer/view/waveformdialog.py @@ -0,0 +1,148 @@ +# UI and connectSignals for MainWindow + +from PySide2 import QtWidgets + +from sohstationviewer.view.core import plottingWidget as plottingWidget + +from sohstationviewer.controller.plottingData import getTitle + +from sohstationviewer.database import extractData + +from sohstationviewer.conf.dbSettings import dbConf + + +class WaveformWidget(plottingWidget.PlottingWidget): + + def plot_channels(self, startTm, endTm, staID, trim_downsample, + dataTime, channelList, timeTicksTotal, + plottingData1, plottingData2): + """ + :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 + + """ + self.trim_downsample = trim_downsample + self.plottingData1 = plottingData1 + self.plottingData2 = plottingData2 + self.processingLog = [] # [(message, type)] + self.errors = [] + if self.axes != []: + self.fig.clear() + self.dateMode = self.parent.dateFormat.upper() + self.timeTicksTotal = timeTicksTotal + self.minX = self.currMinX = max(dataTime[0], startTm) + self.maxX = self.currMaxX = min(dataTime[1], endTm) + self.plotNo = len(self.plottingData1) + len(self.plottingData2) + title = getTitle(staID, self.minX, self.maxX, self.dateMode) + self.plottingBot = plottingWidget.BOTTOM + self.plottingBotPixel = plottingWidget.BOTTOM_PX + self.axes = [] + + self.timestampBarTop = self.add_timestamp_bar(0.003) + self.set_title(title) + + for chanID in self.plottingData1: + print("wf chanID:", chanID) + chanDB = extractData.getWFPlotInfo(chanID) + if chanDB['plotType'] == '': + continue + self.plottingData1[chanID]['chanDB'] = chanDB + self.getZoomData(self.plottingData1[chanID], chanID, True) + for chanID in self.plottingData2: + print("masspos2 chanID:", chanID) + chanDB = extractData.getChanPlotInfo(chanID, self.parent.dataType) + self.plottingData2[chanID]['chanDB'] = chanDB + self.getZoomData(self.plottingData2[chanID], chanID, True) + + 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.plottingBotPixel: + self.widgt.setFixedHeight(self.plottingBotPixel) + + self.draw() + + def getZoomData(self, cData, chanID, firsttime=False): + """ + :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)...] + """ + chanDB = cData['chanDB'] + plotType = chanDB['plotType'] + # data already processed for massposition in plottingWidget + if not (chanID.startswith('VM') or chanID.startswith('MP')): + self.trim_downsample( + cData, self.currMinX, self.currMaxX, firsttime) + self.applyConvertFactor(cData, 1) + # use ax_wf because with massposition, ax has been used + # in plottingWidget + if 'ax_wf' not in cData: + ax = getattr(self, dbConf['plotFunc'][plotType][1])( + cData, chanDB, chanID, None, None) + if ax is None: + return + cData['ax_wf'] = ax + ax.chan = chanID + self.axes.append(ax) + else: + getattr(self, dbConf['plotFunc'][plotType][1])( + cData, chanDB, chanID, cData['ax_wf'], None) + + +class WaveformDialog(QtWidgets.QWidget): + def __init__(self, parent): + super().__init__() + self.parent = parent + self.dateFormat = self.parent.dateFormat + self.bitweightOpt = self.parent.bitweightOpt + self.massPosVoltRangeOpt = self.parent.massPosVoltRangeOpt + self.resize(1000, 700) + self.setWindowTitle("Raw Data Plot") + + mainLayout = QtWidgets.QVBoxLayout() + self.setLayout(mainLayout) + mainLayout.setContentsMargins(5, 5, 5, 5) + mainLayout.setSpacing(0) + + self.plottingWidget = WaveformWidget(self, "waveformWidget") + mainLayout.addWidget(self.plottingWidget, 2) + + bottomLayout = QtWidgets.QHBoxLayout() + mainLayout.addLayout(bottomLayout) + + self.writePSButton = QtWidgets.QPushButton('Write .ps', self) + bottomLayout.addWidget(self.writePSButton) + self.trackingInfoTextBrowser = QtWidgets.QTextBrowser(self) + self.trackingInfoTextBrowser.setFixedHeight(60) + bottomLayout.addWidget(self.trackingInfoTextBrowser) + + self.connectSignals() + + def setData(self, dataType, fileName): + self.dataType = dataType + self.setWindowTitle("Raw Data Plot %s - %s" % (dataType, fileName)) + + def resizeEvent(self, event): + self.plottingWidget.init_size() + + def connectSignals(self): + print("connectSignals")