#!/usr/bin/env seiscomp-python # -*- coding: utf-8 -*- ############################################################################ # Copyright (C) GFZ Potsdam # # All rights reserved. # # # # GNU Affero General Public License Usage # # This file may be used under the terms of the GNU Affero # # Public License version 3.0 as published by the Free Software Foundation # # and appearing in the file LICENSE included in the packaging of this # # file. Please review the following information to ensure the GNU Affero # # Public License version 3.0 requirements will be met: # # https://www.gnu.org/licenses/agpl-3.0.html. # ############################################################################ import sys import seiscomp.client import seiscomp.io import seiscomp.math import seiscomp.datamodel import seiscomp.logging import seiscomp.seismology from seiscomp.datamodel import QMLTypeMapper def time2str(time): """ Convert a seiscomp.core.Time to a string """ return time.toString("%Y-%m-%d %H:%M:%S.%f000000")[:23] def lat2str(lat, enhanced=False): if enhanced: s = f"{abs(lat):.5f} " else: s = f"{abs(lat):.2f} " if lat >= 0: s += "N" else: s += "S" return s def lon2str(lon, enhanced=False): if enhanced: s = f"{abs(lon):.5f} " else: s = f"{abs(lon):.2f} " if lon >= 0: s += "E" else: s += "W" return s def stationCount(org, minArrivalWeight): count = 0 for i in range(org.arrivalCount()): arr = org.arrival(i) # if eight()> 0.5: wt = arrivalWeight(arr) if wt >= minArrivalWeight: count += 1 return count def uncertainty(quantity): # for convenience/readability: get uncertainty from a quantity try: err = 0.5 * (quantity.lowerUncertainty() + quantity.upperUncertainty()) except ValueError: try: err = quantity.uncertainty() except ValueError: err = None return err def arrivalWeight(arrival): # guess arrival weight if not available wt = 0 try: wt = arrival.weight() except ValueError: if ( arrival.timeUsed() or arrival.horizontalSlownessUsed() or arrival.backazimuthUsed() ): wt = 1 return wt def createKML(mode): if mode == "open": text = '' text += '' text += "SeisComP event parameters" text += "SeisComP: earthquakes" text += '" elif mode == "close": text = "" else: return False return text class Bulletin(object): def __init__(self, dbq, long=True): self._dbq = dbq self._long = long self._evt = None self.format = "autoloc1" self.enhanced = False self.polarities = False self.useEventAgencyID = False self.distInKM = False self.minDepthPhaseCount = 3 self.minArrivalWeight = 0.5 self.minStationMagnitudeWeight = 0.5 def _getArrivalsSorted(self, org): # returns arrival list sorted by distance arrivals = [org.arrival(i) for i in range(org.arrivalCount())] return sorted(arrivals, key=lambda t: t.distance()) def _getPicks(self, org): if self._dbq is None: return {} orid = org.publicID() pick = {} for obj in self._dbq.getPicks(orid): p = seiscomp.datamodel.Pick.Cast(obj) key = p.publicID() pick[key] = p return pick def _getAmplitudes(self, org): if self._dbq is None: return {} orid = org.publicID() ampl = {} for obj in self._dbq.getAmplitudesForOrigin(orid): amp = seiscomp.datamodel.Amplitude.Cast(obj) key = amp.publicID() ampl[key] = amp return ampl def _printOriginAutoloc3(self, org, extra=False): orid = org.publicID() arrivals = self._getArrivalsSorted(org) pick = self._getPicks(org) ampl = self._getAmplitudes(org) try: depthPhaseCount = org.quality().depthPhaseCount() except ValueError: depthPhaseCount = 0 for arr in arrivals: wt = arrivalWeight(arr) pha = arr.phase().code() # if (pha[0] in ["p","s"] and wt >= 0.5 ): if pha[0] in ["p", "s"] and wt >= self.minArrivalWeight: depthPhaseCount += 1 txt = "" evt = self._evt if not evt and self._dbq: evt = self._dbq.getEvent(orid) if evt: txt += "Event:\n" txt += f" Public ID {evt.publicID()}\n" if extra: txt += f" Preferred Origin ID {evt.preferredOriginID()}\n" txt += f" Preferred Magnitude ID {evt.preferredMagnitudeID()}\n" try: evtType = evt.type() txt += ( " Type %s\n" % seiscomp.datamodel.EEventTypeNames.name(evtType) ) except ValueError: seiscomp.logging.debug( f"{evt.publicID()}: ignoring empty or unknown event type" ) txt += " Description\n" for i in range(evt.eventDescriptionCount()): evtd = evt.eventDescription(i) evtdtype = seiscomp.datamodel.EEventDescriptionTypeNames.name( evtd.type() ) txt += f" {evtdtype}: {evtd.text()}" if extra: try: txt += ( "\n Creation time %s\n" % evt.creationInfo() .creationTime() .toString("%Y-%m-%d %H:%M:%S") ) except ValueError: pass txt += "\n" preferredMagnitudeID = evt.preferredMagnitudeID() else: preferredMagnitudeID = "" tim = org.time().value() lat = org.latitude().value() lon = org.longitude().value() dep = org.depth().value() timerr = uncertainty(org.time()) laterr = uncertainty(org.latitude()) lonerr = uncertainty(org.longitude()) deperr = uncertainty(org.depth()) tstr = time2str(tim) originHeader = "Origin:\n" if evt: if org.publicID() != evt.preferredOriginID(): originHeader = "Origin (NOT the preferred origin of this event):\n" txt += originHeader if extra: txt += f" Public ID {org.publicID()}\n" txt += f" Date {tstr[:10]}\n" if timerr: if self.enhanced: txt += f" Time {tstr[11:]} +/- {timerr:8.3f} s\n" else: txt += " Time %s +/- %6.1f s\n" % ( tstr[11:-2], timerr, ) else: if self.enhanced: txt += f" Time {tstr[11:]}\n" else: txt += f" Time {tstr[11:-2]}\n" if laterr: if self.enhanced: txt += " Latitude %10.5f deg +/- %8.3f km\n" % ( lat, laterr, ) else: txt += " Latitude %7.2f deg +/- %6.0f km\n" % ( lat, laterr, ) else: if self.enhanced: txt += f" Latitude {lat:10.5f} deg\n" else: txt += f" Latitude {lat:7.2f} deg\n" if lonerr: if self.enhanced: txt += " Longitude %10.5f deg +/- %8.3f km\n" % ( lon, lonerr, ) else: txt += " Longitude %7.2f deg +/- %6.0f km\n" % ( lon, lonerr, ) else: if self.enhanced: txt += f" Longitude {lon:10.5f} deg\n" else: txt += f" Longitude {lon:7.2f} deg\n" if self.enhanced: txt += f" Depth {dep:11.3f} km" else: txt += f" Depth {dep:7.0f} km" if deperr is None: txt += "\n" elif deperr == 0: txt += " (fixed)\n" else: if depthPhaseCount >= self.minDepthPhaseCount: if self.enhanced: txt += " +/- %8.3f km (%d depth phases)\n" % ( deperr, depthPhaseCount, ) else: txt += " +/- %4.0f km (%d depth phases)\n" % ( deperr, depthPhaseCount, ) else: if self.enhanced: txt += f" +/- {deperr:8.3f} km\n" else: txt += f" +/- {deperr:4.0f} km\n" agencyID = "" if self.useEventAgencyID: try: agencyID = evt.creationInfo().agencyID() except ValueError: pass else: try: agencyID = org.creationInfo().agencyID() except ValueError: pass txt += f" Agency {agencyID}\n" if extra: try: authorID = org.creationInfo().author() except ValueError: authorID = "NOT SET" txt += f" Author {authorID}\n" txt += " Mode " try: txt += "%s\n" % seiscomp.datamodel.EEvaluationModeNames.name( org.evaluationMode() ) except ValueError: txt += "NOT SET\n" txt += " Status " try: txt += "%s\n" % seiscomp.datamodel.EEvaluationStatusNames.name( org.evaluationStatus() ) except ValueError: txt += "NOT SET\n" if extra: txt += " Creation time " try: txt += "%s\n" % org.creationInfo().creationTime().toString( "%Y-%m-%d %H:%M:%S" ) except ValueError: txt += "NOT SET\n" try: if self.enhanced: txt += ( " Residual RMS %9.3f s\n" % org.quality().standardError() ) else: txt += ( " Residual RMS %6.2f s\n" % org.quality().standardError() ) except ValueError: pass try: if self.enhanced: txt += ( " Azimuthal gap %8.1f deg\n" % org.quality().azimuthalGap() ) else: txt += ( " Azimuthal gap %5.0f deg\n" % org.quality().azimuthalGap() ) except ValueError: pass txt += "\n" networkMagnitudeCount = org.magnitudeCount() networkMagnitudes = {} # Each station magnitude contributes to the network # magnitude of the same type. # # We save here the StationMagnitudeContribution objects # by publicID of the corresponding StationMagnitude object. stationMagnitudeContributions = {} tmptxt = txt txt = "" foundPrefMag = False for i in range(networkMagnitudeCount): mag = org.magnitude(i) val = mag.magnitude().value() typ = mag.type() networkMagnitudes[typ] = mag for k in range(mag.stationMagnitudeContributionCount()): smc = mag.stationMagnitudeContribution(k) smid = smc.stationMagnitudeID() stationMagnitudeContributions[smid] = smc err = uncertainty(mag.magnitude()) if err is not None: err = f"+/- {err:.2f}" else: err = "" if mag.publicID() == preferredMagnitudeID: preferredMarker = "preferred" foundPrefMag = True else: preferredMarker = " " if extra: try: agencyID = mag.creationInfo().agencyID() except ValueError: pass else: agencyID = "" txt += " %-8s %5.2f %8s %3d %s %s\n" % ( typ, val, err, mag.stationCount(), preferredMarker, agencyID, ) if not foundPrefMag and preferredMagnitudeID != "": mag = seiscomp.datamodel.Magnitude.Find(preferredMagnitudeID) if mag is None and self._dbq: o = self._dbq.loadObject( seiscomp.datamodel.Magnitude.TypeInfo(), preferredMagnitudeID ) mag = seiscomp.datamodel.Magnitude.Cast(o) if mag: val = mag.magnitude().value() typ = mag.type() networkMagnitudes[typ] = mag err = uncertainty(mag.magnitude()) if err is not None: err = f"+/- {err:.2f}" else: err = "" preferredMarker = "preferred" if extra: try: agencyID = mag.creationInfo().agencyID() except ValueError: pass else: agencyID = "" txt += " %-8s %5.2f %8s %3d %s %s\n" % ( typ, val, err, mag.stationCount(), preferredMarker, agencyID, ) txt = tmptxt + "%d Network magnitudes:\n" % networkMagnitudeCount + txt if not self._long: return txt lineFMT = " %-5s %-2s " if self.enhanced: lineFMT += "%9.3f" if self.distInKM else "%9.5f" else: lineFMT += "%5.0f" if self.distInKM else "%5.1f" lineFMT += " %s %-7s %s %s %1s%1s %3.1f " if self.polarities: lineFMT += "%s " lineFMT += "%-5s\n" dist_azi = {} lines = [] for arr in arrivals: p = seiscomp.datamodel.Pick.Find(arr.pickID()) if p is None: lines.append((180, f" ## missing pick {arr.pickID()}\n")) continue if self.distInKM: dist = seiscomp.math.deg2km(arr.distance()) else: dist = arr.distance() wfid = p.waveformID() net = wfid.networkCode() sta = wfid.stationCode() if self.enhanced: try: azi = f"{arr.azimuth():5.1f}" except ValueError: azi = " N/A" tstr = time2str(p.time().value())[11:] try: res = f"{arr.timeResidual():7.3f}" except ValueError: res = " N/A" else: try: azi = f"{arr.azimuth():3.0f}" except ValueError: azi = "N/A" tstr = time2str(p.time().value())[11:-2] try: res = f"{arr.timeResidual():5.1f}" except ValueError: res = " N/A" dist_azi[net + "_" + sta] = (dist, azi) wt = arrivalWeight(arr) pha = arr.phase().code() flag = "X "[wt > 0.1] try: status = seiscomp.datamodel.EEvaluationModeNames.name( p.evaluationMode() )[0].upper() except ValueError: status = "-" if self.polarities: try: pol = seiscomp.datamodel.EPickPolarityNames.name(p.polarity()) except ValueError: pol = None if pol: if pol == "positive": pol = "u" elif pol == "negative": pol = "d" elif pol == "undecidable": pol = "x" else: pol = "." else: pol = "." line = lineFMT % ( sta, net, dist, azi, pha, tstr, res, status, flag, wt, pol, sta, ) else: line = lineFMT % ( sta, net, dist, azi, pha, tstr, res, status, flag, wt, sta, ) lines.append((dist, line)) lines.sort() txt += "\n" txt += "%d Phase arrivals:\n" % org.arrivalCount() if self.enhanced: txt += ( " sta net dist azi phase time res wt " ) else: txt += " sta net dist azi phase time res wt " if self.polarities: txt += " " txt += "sta \n" for dist, line in lines: txt += line txt += "\n" stationMagnitudeCount = org.stationMagnitudeCount() activeStationMagnitudeCount = 0 stationMagnitudes = {} for i in range(stationMagnitudeCount): mag = org.stationMagnitude(i) typ = mag.type() if typ not in networkMagnitudes: continue if typ not in stationMagnitudes: stationMagnitudes[typ] = [] # suppress unused station magnitudes smid = mag.publicID() if not smid in stationMagnitudeContributions: continue try: w = stationMagnitudeContributions[smid].weight() except ValueError: w = self.minStationMagnitudeWeight if w < self.minStationMagnitudeWeight: continue stationMagnitudes[typ].append(mag) activeStationMagnitudeCount += 1 lineFMT = " %-5s %-2s " if self.enhanced: lineFMT += "%9.3f" if self.distInKM else "%9.5f" else: lineFMT += "%5.0f" if self.distInKM else "%5.1f" lineFMT += " %s %-6s %5.2f %5.2f %8s %4s\n" lines = [] for typ in stationMagnitudes: for mag in stationMagnitudes[typ]: key = mag.amplitudeID() if key: amp = seiscomp.datamodel.Amplitude.Find(key) if amp is None and self._dbq: seiscomp.logging.debug(f"missing station amplitude '{key}'") # try to load amplitude from database obj = self._dbq.loadObject( seiscomp.datamodel.Amplitude.TypeInfo(), key ) amp = seiscomp.datamodel.Amplitude.Cast(obj) else: amp = None # This is expected behaviour for some magnitudes like Me. p = "" a = "N/A" if amp: try: a = f"{amp.amplitude().value():g}" except ValueError: a = "N/A" if typ in ["mb", "Ms", "Ms(BB)"]: try: p = f"{amp.period().value():.2f}" except ValueError: p = "N/A" else: p = "" wfid = mag.waveformID() net = wfid.networkCode() sta = wfid.stationCode() try: dist, azi = dist_azi[net + "_" + sta] except KeyError: dist, azi = 0, " N/A" if self.enhanced else "N/A" val = mag.magnitude().value() res = val - networkMagnitudes[typ].magnitude().value() line = lineFMT % (sta, net, dist, azi, typ, val, res, a, p) lines.append((dist, line)) lines.sort() if activeStationMagnitudeCount: txt += "%d Station magnitudes:\n" % activeStationMagnitudeCount if self.enhanced: txt += " sta net dist azi type value res amp per\n" else: txt += " sta net dist azi type value res amp per\n" for dist, line in lines: txt += line else: txt += "No station magnitudes\n" return txt def _printOriginAutoloc1(self, org): evt = self._evt if not evt and self._dbq: evt = self._dbq.getEvent(org.publicID()) if evt: evid = evt.publicID() pos = evid.find("#") # XXX Hack!!! if pos != -1: evid = evid[:pos] prefMagID = evt.preferredMagnitudeID() else: evid = "..." prefMagID = "" txt = "" if self.enhanced: depth = org.depth().value() sTime = org.time().value().toString("%Y/%m/%d %H:%M:%S.%f00")[:24] else: depth = int(org.depth().value() + 0.5) sTime = org.time().value().toString("%Y/%m/%d %H:%M:%S.%f")[:22] tmp = { "evid": evid, "nsta": stationCount(org, self.minArrivalWeight), "time": sTime, "lat": lat2str(org.latitude().value(), self.enhanced), "lon": lon2str(org.longitude().value(), self.enhanced), "dep": depth, "reg": seiscomp.seismology.Regions.getRegionName( org.latitude().value(), org.longitude().value() ), # changed to properly report location method. (Marco Olivieri 21/06/2010) "method": org.methodID(), "model": org.earthModelID(), # end (MO) "stat": "A", } try: if org.evaluationMode() == seiscomp.datamodel.MANUAL: tmp["stat"] = "M" except ValueError: pass # dummy default tmp["mtyp"] = "M" tmp["mval"] = 0.0 foundMag = False networkMagnitudeCount = org.magnitudeCount() for i in range(networkMagnitudeCount): mag = org.magnitude(i) if mag.publicID() == prefMagID: tmp["mtyp"] = mag.type() tmp["mval"] = mag.magnitude().value() foundMag = True break if not foundMag and prefMagID != "": mag = seiscomp.datamodel.Magnitude.Find(prefMagID) if mag is None and self._dbq: o = self._dbq.loadObject( seiscomp.datamodel.Magnitude.TypeInfo(), prefMagID ) mag = seiscomp.datamodel.Magnitude.Cast(o) if mag: tmp["mtyp"] = mag.type() tmp["mval"] = mag.magnitude().value() # changed to properly report location method. (Marco Olivieri 21/06/2010) # txt += """ # Autoloc alert %(evid)s: determined by %(nsta)d stations, type %(stat)s # # LOCSAT solution (with start solution, %(nsta)d stations used, weight %(nsta)d): # # %(reg)s %(mtyp)s=%(mval).1f %(time)s %(lat)s %(lon)s %(dep)d km # # Stat Net Date Time Amp Per Res Dist Az mb ML mB # """ % tmp txtFMT = """ Alert %(evid)s: determined by %(nsta)d stations, type %(stat)s %(method)s solution with earthmodel %(model)s (with start solution, %(nsta)d stations used, weight %(nsta)d): %(reg)s %(mtyp)s=%(mval).1f %(time)s %(lat)s %(lon)s %(dep)""" if self.enhanced: txtFMT += """.3f km Stat Net Date Time Amp Per Res Dist Az mb ML mB """ else: txtFMT += """d km Stat Net Date Time Amp Per Res Dist Az mb ML mB """ txt += txtFMT % tmp # end (MO) arrivals = self._getArrivalsSorted(org) pick = self._getPicks(org) ampl = self._getAmplitudes(org) stationMagnitudeCount = org.stationMagnitudeCount() stationMagnitudes = {} for i in range(stationMagnitudeCount): mag = org.stationMagnitude(i) typ = mag.type() if typ == "MLv" or typ == "MLh": typ = "ML" if typ not in stationMagnitudes: stationMagnitudes[typ] = {} sta = mag.waveformID().stationCode() stationMagnitudes[typ][sta] = mag lineFMT = " %-5s %-2s %s %10.1f %4.1f %s " if self.enhanced: lineFMT += "%9.3f" if self.distInKM else "%9.5f" else: lineFMT += "%5.0f" if self.distInKM else "%5.1f" lineFMT += " %s%s\n" for arr in arrivals: wt = arrivalWeight(arr) if wt < self.minArrivalWeight: continue if self.distInKM: dist = seiscomp.math.deg2km(arr.distance()) else: dist = arr.distance() p = seiscomp.datamodel.Pick.Find(arr.pickID()) if p is None: txt += f" ## missing pick {arr.pickID()}\n" continue wfid = p.waveformID() net = wfid.networkCode() sta = wfid.stationCode() if self.enhanced: tstr = p.time().value().toString("%y/%m/%d %H:%M:%S.%f00")[:22] try: res = f"{arr.timeResidual():7.3f}" except ValueError: res = " N/A" try: azi = f"{arr.azimuth():5.1f}" except ValueError: azi = " N/A" else: tstr = p.time().value().toString("%y/%m/%d %H:%M:%S.%f")[:20] try: res = f"{arr.timeResidual():5.1f}" except ValueError: res = " N/A" try: azi = f"{arr.azimuth():3.0f}" except ValueError: azi = "N/A" pha = arr.phase().code() mstr = "" amp = per = 0.0 for typ in ["mb", "ML", "mB"]: mag = 0.0 try: m = stationMagnitudes[typ][sta] mag = m.magnitude().value() if typ == "mb": ampid = m.amplitudeID() a = seiscomp.datamodel.Amplitude.Find(ampid) if a is None and self._dbq: obj = self._dbq.loadObject( seiscomp.datamodel.Amplitude.TypeInfo(), ampid ) a = seiscomp.datamodel.Amplitude.Cast(obj) if a: per = a.period().value() try: amp = a.amplitude().value() except ValueError: amp = -1 except KeyError: pass mstr += f" {mag:3.1f}" txt += lineFMT % (sta, net, tstr, amp, per, res, dist, azi, mstr) if self.enhanced: txt += f"\n RMS-ERR: {org.quality().standardError():.3f}\n\n" else: txt += f"\n RMS-ERR: {org.quality().standardError():.2f}\n\n" if evt: try: if self.enhanced: tm = ( evt.creationInfo() .creationTime() .toString("%Y/%m/%d %H:%M:%S.%f") ) else: tm = evt.creationInfo().creationTime().toString("%Y/%m/%d %H:%M:%S") txt += f" Event created: {tm}\n" except ValueError: pass try: if self.enhanced: tm = org.creationInfo().creationTime().toString("%Y/%m/%d %H:%M:%S.%f") else: tm = org.creationInfo().creationTime().toString("%Y/%m/%d %H:%M:%S") txt += f" This origin created: {tm}\n" except ValueError: pass return txt def _printOriginKML(self, org): txt = "" evt = self._evt if not evt and self._dbq: evt = self._dbq.getEvent(org.publicID()) author = None agencyID = None eType = None prefMagID = None depth = None altitude = None sTime = None mode = None status = None if evt: evid = evt.publicID() pos = evid.find("#") # XXX Hack!!! if pos != -1: evid = evid[:pos] try: prefMagID = evt.preferredMagnitudeID() except ValueError: pass try: author = org.creationInfo().author() except ValueError: pass try: mode = seiscomp.datamodel.EEvaluationModeNames.name( org.evaluationMode() ) except ValueError: pass try: status = seiscomp.datamodel.EEvaluationStatusNames.name( org.evaluationStatus() ) except ValueError: pass if self.useEventAgencyID: try: agencyID = evt.creationInfo().agencyID() except ValueError: pass else: try: agencyID = org.creationInfo().agencyID() except ValueError: pass try: eType = seiscomp.datamodel.EEventTypeNames.name(evt.type()) except ValueError: pass else: evid = "" prefMagID = "" if evt: ID = evt.publicID() else: ID = org.publicID() if self.enhanced: latitude = f"{org.latitude().value():.3f}" longitude = f"{org.longitude().value():.3f}" depth = float(f"{org.depth().value():.3f}") sTime = org.time().value().toString("%FT%T.%f")[:23] else: latitude = f"{org.latitude().value():.1f}" longitude = f"{org.longitude().value():.1f}" depth = float(f"{org.depth().value():.0f}") sTime = org.time().value().toString("%FT%T.%f")[:21] if depth: altitude = -1.0 * float(depth) try: region = seiscomp.seismology.Regions.getRegionName( org.latitude().value(), org.longitude().value() ) except ValueError: region = "" mag = False mType = "" mVal = None networkMagnitudeCount = org.magnitudeCount() for i in range(networkMagnitudeCount): mag = org.magnitude(i) if mag.publicID() == prefMagID: mType = mag.type() mVal = f"{mag.magnitude().value():.1f}" break if not mag and prefMagID != "": mag = seiscomp.datamodel.Magnitude.Find(prefMagID) if mag is None and self._dbq: o = self._dbq.loadObject( seiscomp.datamodel.Magnitude.TypeInfo(), prefMagID ) mag = seiscomp.datamodel.Magnitude.Cast(o) if mag: mType = mag.type() mVal = f"{mag.magnitude().value():.1f}" txt = "" txt += "" if eType: txt += f'{eType}' txt += f'{sTime}' if mVal: txt += f'{mVal}' else: txt += 'None' if len(region) > 0: txt += f'{region}' txt += f'{latitude}' txt += f'{longitude}' txt += f'{depth}' if agencyID: txt += f'{agencyID}' if author: txt += f'{author}' if mode: txt += f'{mode}' if status: txt += f'{status}' txt += "" txt += f"{ID}" txt += "#earthquake" # use icon scaling like in SC but devide by 10 if mVal: size = 4.9 * (float(mVal) - 1.2) / 10.0 else: size = 1 # scale symbolColor with depth # global default: 0:FF0000,50:ffA500,100:FFFF00,250:00FF00,600:0000FF if depth < 50: symbolColor = "ff0000ff" elif depth >= 50 and depth < 100: symbolColor = "ff00a5ff" elif depth >= 100 and depth < 250: symbolColor = "ff00ffff" elif depth >= 250 and depth < 600: symbolColor = "ff00ff00" else: symbolColor = "ffff0000" txt += f"" txt += f"{longitude},{latitude},{altitude}" txt += f"{sTime}" txt += "" return txt def _printOriginFDSN(self, org): evt = self._evt if not evt and self._dbq: evt = self._dbq.getEvent(org.publicID()) author, agencyID, eType, prefMagID = "", "", "", "" if evt: evid = evt.publicID() pos = evid.find("#") # XXX Hack!!! if pos != -1: evid = evid[:pos] try: prefMagID = evt.preferredMagnitudeID() except ValueError: pass try: author = org.creationInfo().author() except ValueError: pass if self.useEventAgencyID: try: agencyID = evt.creationInfo().agencyID() except ValueError: pass else: try: agencyID = org.creationInfo().agencyID() except ValueError: pass try: eType = QMLTypeMapper.EventTypeToString(evt.type()) except ValueError: pass else: evid = "" prefMagID = "" if self.enhanced: depth = f"{org.depth().value():.3f}" sTime = org.time().value().toString("%FT%T.%f")[:26] else: depth = f"{org.depth().value():.0f}" sTime = org.time().value().toString("%FT%T.%f")[:23] lat = lat2str(org.latitude().value(), self.enhanced) lon = lon2str(org.longitude().value(), self.enhanced) try: region = seiscomp.seismology.Regions.getRegionName( org.latitude().value(), org.longitude().value() ) except ValueError: region = "" foundMag = False mType, mVal, mAuthor = "", "", "" networkMagnitudeCount = org.magnitudeCount() for i in range(networkMagnitudeCount): mag = org.magnitude(i) if mag.publicID() == prefMagID: mType = mag.type() mVal = f"{mag.magnitude().value():.1f}" foundMag = True break if not foundMag and prefMagID != "": mag = seiscomp.datamodel.Magnitude.Find(prefMagID) if mag is None and self._dbq: o = self._dbq.loadObject( seiscomp.datamodel.Magnitude.TypeInfo(), prefMagID ) mag = seiscomp.datamodel.Magnitude.Cast(o) if mag: mType = mag.type() mVal = f"{mag.magnitude().value():.1f}" try: mAuthor = mag.creationInfo().author() except ValueError: pass txt = "%s|%s|%s|%s|%s|%s||%s|%s|%s|%s|%s|%s|%s\n" % ( evid, sTime, lat, lon, depth, author, agencyID, evid, mType, mVal, mAuthor, region, eType, ) return txt def printOrigin(self, origin): org = None if isinstance(origin, seiscomp.datamodel.Origin): org = origin elif isinstance(origin, str): if self._dbq: org = self._dbq.loadObject(seiscomp.datamodel.Origin.TypeInfo(), origin) org = seiscomp.datamodel.Origin.Cast(org) if not org: raise KeyError("Unknown origin " + origin) else: raise TypeError("illegal type for origin") if self.format == "fdsnws": return self._printOriginFDSN(org) elif self.format == "autoloc1": return self._printOriginAutoloc1(org) elif self.format == "autoloc3": return self._printOriginAutoloc3(org, extra=False) elif self.format == "autoloc3extra": return self._printOriginAutoloc3(org, extra=True) if self.format == "kml": return self._printOriginKML(org) else: pass def printEvent(self, event): try: evt = None if isinstance(event, seiscomp.datamodel.Event): self._evt = event org = seiscomp.datamodel.Origin.Find(event.preferredOriginID()) if not org: org = event.preferredOriginID() return self.printOrigin(org) elif isinstance(event, str): if self._dbq: evt = self._dbq.loadObject( seiscomp.datamodel.Event.TypeInfo(), event ) evt = seiscomp.datamodel.Event.Cast(evt) self._evt = evt if evt is None: raise KeyError("unknown event " + event) return self.printOrigin(evt.preferredOriginID()) else: raise TypeError("illegal type for event") finally: self._evt = None class BulletinApp(seiscomp.client.Application): def __init__(self, argc, argv): seiscomp.client.Application.__init__(self, argc, argv) self.setMessagingEnabled(False) self.setDatabaseEnabled(True, True) self.setDaemonEnabled(False) self.setLoggingToStdErr(True) self.setLoadRegionsEnabled(True) self.format = "autoloc1" self.inputFile = None def createCommandLineDescription(self): try: self.commandline().addGroup("Input") self.commandline().addStringOption( "Input", "format,f", "Input format to use (xml [default], zxml (zipped xml), binary).", ) self.commandline().addStringOption( "Input", "input,i", "Input file, default: stdin." ) self.commandline().addGroup("Dump") self.commandline().addStringOption( "Dump", "event,E", "ID of event(s) to dump. Separate multiple IDs by comma.", ) self.commandline().addStringOption( "Dump", "origin,O", "ID of origin(s) to dump. Separate multiple IDs by comma.", ) self.commandline().addOption( "Dump", "event-agency-id", "Use agency ID information from event instead of preferred origin.", ) self.commandline().addOption( "Dump", "first-only", "Dump only the first event/origin. " "Expects input from file or stdin.", ) self.commandline().addOption( "Dump", "polarities,p", "Dump onset polarities." ) self.commandline().addStringOption( "Dump", "weight,w", "Weight threshold for printed and counted picks." ) self.commandline().addGroup("Output") self.commandline().addOption("Output", "autoloc1,1", "Format: autoloc1.") self.commandline().addOption("Output", "autoloc3,3", "Format: autoloc3.") self.commandline().addOption( "Output", "fdsnws,4", "Format: FDSNWS event text, e.g., for generating catalogs.", ) self.commandline().addOption( "Output", "kml,5", "Format: KML, GIS file format.", ) self.commandline().addOption( "Output", "enhanced,e", "Enhanced output precision for local earthquakes.", ) self.commandline().addOption( "Output", "dist-in-km,k", "Print distances in km instead of degree." ) self.commandline().addStringOption( "Output", "output,o", "Name of output file. If not given, all event parameters are printed to stdout.", ) self.commandline().addOption( "Output", "extra,x", "Extra detailed autoloc3 format." ) except RuntimeError: seiscomp.logging.warning(f"caught unexpected error {sys.exc_info()}") return True def validateParameters(self): if not seiscomp.client.Application.validateParameters(self): return False try: self.inputFile = self.commandline().optionString("input") except RuntimeError: self.inputFile = None if self.inputFile: self.setDatabaseEnabled(False, False) if not self.commandline().hasOption( "event" ) and not self.commandline().hasOption("origin"): self.setDatabaseEnabled(False, False) return True def printUsage(self): print( """Usage: scbulletin [options] Generate bulletins from events or origins in various formats: autoloc1, autoloc3, fdsnws, kml.""" ) seiscomp.client.Application.printUsage(self) print( """Examples: Create a bulletin from one event in the seiscomp database scbulletin -d mysql://sysop:sysop@localhost/seiscomp -E gempa2012abcd Create a bulletin from event parameters in XML scbulletin -i gempa2012abcd.xml """ ) return True def run(self): evid = None orid = None mw = None inputFile = None txt = None outputFile = None try: evid = self.commandline().optionString("event") except RuntimeError: pass try: orid = self.commandline().optionString("origin") except RuntimeError: pass if evid != "" or orid != "" or not self.inputFile: dbq = seiscomp.datamodel.DatabaseQuery(self.database()) else: dbq = None bulletin = Bulletin(dbq) bulletin.format = "autoloc1" try: mw = self.commandline().optionString("weight") except RuntimeError: pass if mw != "" and mw is not None: bulletin.minArrivalWeight = float(mw) if self.commandline().hasOption("autoloc1"): bulletin.format = "autoloc1" elif self.commandline().hasOption("autoloc3"): if self.commandline().hasOption("extra"): bulletin.format = "autoloc3extra" else: bulletin.format = "autoloc3" if self.commandline().hasOption("fdsnws"): bulletin.format = "fdsnws" if self.commandline().hasOption("kml"): bulletin.format = "kml" if self.commandline().hasOption("enhanced"): bulletin.enhanced = True if self.commandline().hasOption("polarities"): bulletin.polarities = True if self.commandline().hasOption("event-agency-id"): bulletin.useEventAgencyID = True if self.commandline().hasOption("dist-in-km"): bulletin.distInKM = True try: outputFile = self.commandline().optionString("output") except RuntimeError: pass inputFile = self.inputFile if not self.inputFile: txt = "" try: if evid: for ev in evid.split(","): try: txt += bulletin.printEvent(ev) except ValueError: seiscomp.logging.error(f"Unknown event '{ev}'") elif orid: for org in orid.split(","): try: txt += bulletin.printOrigin(org) except ValueError: seiscomp.logging.error(f"Unknown origin '{org}'") else: inputFile = "-" print("Expecting input in XML from stdin", file=sys.stderr) except Exception as exc: print(f"ERROR: {exc}", file=sys.stderr) # return False else: inputFile = self.inputFile if inputFile: inputFormat = "xml" try: try: inputFormat = self.commandline().optionString("format") except RuntimeError: pass if inputFormat == "xml": ar = seiscomp.io.XMLArchive() elif inputFormat == "zxml": ar = seiscomp.io.XMLArchive() ar.setCompression(True) elif inputFormat == "binary": ar = seiscomp.io.BinaryArchive() else: raise TypeError("unknown input format: " + inputFormat) if not ar.open(inputFile): raise IOError("Unable to open input file " + inputFile) obj = ar.readObject() if obj is None: raise IOError("Invalid format in " + inputFile) ep = seiscomp.datamodel.EventParameters.Cast(obj) if ep is None: raise TypeError("No event parameters found in " + inputFile) if ep.eventCount() <= 0: if ep.originCount() <= 0: raise TypeError( "No origin and no event in event " "parameters found in " + inputFile ) if self.commandline().hasOption("first-only"): org = ep.origin(0) txt = bulletin.printOrigin(org) else: txt = "" for i in range(ep.originCount()): org = ep.origin(i) if orid and org.publicID() not in orid: seiscomp.logging.error( "%s: Skipping origin with ID %s" % (inputFile, org.publicID()) ) continue txt += bulletin.printOrigin(org) else: if self.commandline().hasOption("first-only"): ev = ep.event(0) if ev is None: raise TypeError("Invalid event in " + inputFile) try: txt = bulletin.printEvent(ev) except KeyError: raise TypeError("Unknown event") elif orid: txt = "" for oid in orid.split(","): org = ep.findOrigin(oid) if org: txt += bulletin.printOrigin(org) else: seiscomp.logging.error( f"{inputFile}: Skipping origin with ID {oid}" ) else: txt = "" for i in range(ep.eventCount()): ev = ep.event(i) if evid and ev.publicID() not in evid: seiscomp.logging.error( "%s: Skipping event with ID %s" % (inputFile, ev.publicID()) ) continue try: txt += bulletin.printEvent(ev) except KeyError: raise TypeError("Unknown event") except Exception as exc: seiscomp.logging.error(f"{str(exc)}") return False if outputFile: print(f"Output data to file: {outputFile}", file=sys.stderr) try: out = open(outputFile, "w") except Exception: print("Cannot create output file {outputFile}", file=sys.stderr) return -1 else: out = sys.stdout if bulletin.format == "fdsnws": print( "#EventID|Time|Latitude|Longitude|Depth/km|Author|" "Catalog|Contributor|ContributorID|MagType|Magnitude|" "MagAuthor|EventLocationName|EventType", file=out, ) if bulletin.format == "kml": text = createKML("open") if text: print(f"{text}", file=out) else: return False if txt: print(f"{txt}", file=out) if bulletin.format == "kml": text = createKML("close") if text: print(f"{text}", file=out) else: return False return True def main(): app = BulletinApp(len(sys.argv), sys.argv) return app() if __name__ == "__main__": sys.exit(main())