diff --git a/gifts/common/xmlConfig.py b/gifts/common/xmlConfig.py index ae9f723..173fc8b 100644 --- a/gifts/common/xmlConfig.py +++ b/gifts/common/xmlConfig.py @@ -53,6 +53,9 @@ CVCTNCLDS = 'SigConvectiveCloudType' NIL = 'nil' RECENTWX = 'AerodromeRecentWeather' +RWYDEPST = '0-20-086' +RWYCNTMS = '0-20-087' +RWYFRCTN = '0-20-089' SEACNDS = '0-22-061' SWX_PHENOMENA = 'SpaceWxPhenomena' SWX_LOCATION = 'SpaceWxLocation' diff --git a/gifts/metarEncoder.py b/gifts/metarEncoder.py index 567f289..8a0b237 100644 --- a/gifts/metarEncoder.py +++ b/gifts/metarEncoder.py @@ -27,7 +27,8 @@ def __init__(self): self._Logger = logging.getLogger(__name__) # # Create dictionaries of the following WMO codes - neededCodes = [des.CLDAMTS, des.WEATHER, des.RECENTWX, des.CVCTNCLDS, des.SEACNDS] + neededCodes = [des.CLDAMTS, des.WEATHER, des.RECENTWX, des.CVCTNCLDS, des.SEACNDS, des.RWYDEPST, des.RWYCNTMS, + des.RWYDEPST, des.RWYFRCTN] try: self.codes = deu.parseCodeRegistryTables(des.CodesFilePath, neededCodes, des.PreferredLanguageForTitles) except AssertionError as msg: # pragma: no cover @@ -38,13 +39,15 @@ def __init__(self): setattr(self, 'vcnty', self.pcp) self.observedTokenList = ['temps', 'altimeter', 'wind', 'vsby', 'rvr', 'pcp', 'obv', 'vcnty', - 'sky', 'rewx', 'ws', 'seastate'] + 'sky', 'rewx', 'ws', 'seastate', 'rwystate'] self.trendTokenList = ['wind', 'pcp', 'obv', 'sky'] self._re_unknwnPcpn = re.compile(r'(?P[-+]?)(?P(SH|FZ|TS))') self._re_cloudLyr = re.compile(r'(VV|FEW|SCT|BKN|OVC|///|CLR|SKC)([/\d]{3})?(CB|TCU|///)?') self._TrendForecast = {'TEMPO': 'TEMPORARY_FLUCTUATIONS', 'BECMG': 'BECOMING'} + self._RunwayDepositDepths = {'92': '100', '93': '150', '94': '200', + '95': '250', '96': '300', '97': '350', '98': '400'} def __call__(self, decodedMetar, tacString): @@ -727,6 +730,91 @@ def seastate(self, parent, token): except KeyError: pass + def rwystate(self, parent, tokens): + + for token in tokens: + + indent1 = ET.SubElement(parent, 'iwxxm:runwayState') + if token['state'] == 'SNOCLO': + indent1.set('nilReason', des.NIL_SNOCLO_URL) + indent1.set('xsi:nil', 'true') + continue + + indent2 = ET.SubElement(indent1, 'iwxxm:AerodromeRunwayState') + indent2.set('allRunways', 'false') + # + # Attributes set first + if len(token['runway']) == 0 or token['runway'] == '88': + indent2.set('allRunways', 'true') + + if token['runway'] == '99': + indent2.set('fromPreviousReport', 'true') + + if token['state'][:4] == 'CLRD': + indent2.set('cleared', 'true') + # + # Runway direction + if indent2.get('allRunways') == 'false': + indent3 = ET.SubElement(indent2, 'iwxxm:runway') + if token['runway'] == '99': + indent3.set('nilReason', self.codes[des.NIL][des.NA][0]) + else: + self.runwayDirection(indent3, token['runway']) + # + # Runway deposits + if token['state'][0].isdigit(): + indent3 = ET.SubElement(indent2, 'iwxxm:depositType') + uri, title = self.codes[des.RWYDEPST][token['state'][0]] + indent3.set('xlink:href', uri) + if (des.TITLES & des.RunwayDeposit): + indent3.set('xlink:title', title) + # + # Runway contaminates + if token['state'][1].isdigit(): + indent3 = ET.SubElement(indent2, 'iwxxm:contamination') + try: + uri, title = self.codes[des.RWYCNTMS][token['state'][1]] + except KeyError: + uri, title = self.codes[des.RWYCNTMS]['15'] + + indent3.set('xlink:href', uri) + if (des.TITLES & des.AffectedRunwayCoverage): + indent3.set('xlink:title', title) + # + # Depth of deposits + indent3 = ET.Element('iwxxm:depthOfDeposit') + depth = token['state'][2:4] + if depth.isdigit(): + if depth != '99': + indent3.set('uom', 'mm') + indent3.text = self._RunwayDepositDepths.get(depth, depth) + else: + indent3.set('uom', 'N/A') + indent3.set('xsi:nil', 'true') + indent3.set('nilReason', self.codes[des.NIL][des.UNKNWN][0]) + + indent2.append(indent3) + + elif depth == '//': + indent3.set('uom', 'N/A') + indent3.set('xsi:nil', 'true') + indent3.set('nilReason', self.codes[des.NIL][des.NOOPRSIG][0]) + indent2.append(indent3) + # + # Runway friction + friction = token['state'][4:6] + if friction.isdigit(): + # + # Remove leading zeros + friction = str(int(friction)) + indent3 = ET.SubElement(indent2, 'iwxxm:estimatedSurfaceFrictionOrBrakingAction') + uri, ignored = self.codes[des.RWYFRCTN][friction] + indent3.set('xlink:href', uri) + if (des.TITLES & des.RunwayFriction): + title = des.RunwayFrictionValues.get(friction, 'Friction coefficient: %.2f' % + (int(friction) * 0.01)) + indent3.set('xlink:title', title) + def runwayDirection(self, parent, rwy): uuid = self.runwayDirectionCache.get(rwy, deu.getUUID()) diff --git a/tests/test_metar_encoding.py b/tests/test_metar_encoding.py index 48b767d..4f00e1b 100644 --- a/tests/test_metar_encoding.py +++ b/tests/test_metar_encoding.py @@ -7,7 +7,8 @@ import gifts.common.xmlConfig as des import gifts.common.xmlUtilities as deu -reqCodes = [des.WEATHER, des.SEACNDS, des.RECENTWX, des.CVCTNCLDS, des.CLDAMTS] +reqCodes = [des.WEATHER, des.SEACNDS, des.RWYFRCTN, des.RWYCNTMS, des.RWYDEPST, des.RECENTWX, des.CVCTNCLDS, + des.CLDAMTS] codes = deu.parseCodeRegistryTables(des.CodesFilePath, reqCodes) @@ -1106,6 +1107,90 @@ def test_seastates(): assert wh.get('nilReason') == notObservable[0] +def test_runwaystates(): + # + # Runway states depreciated, discontinued Nov 2021. Tests are not exhaustive. + # + test = """SAXX99 XXXX 151200 +METAR BIAR 290000Z /////KT //// // ////// ///// Q//// R/SNOCLO= +METAR BIAR 290000Z /////KT //// // ////// ///// Q//// R/CLRD//= +METAR BIAR 290000Z /////KT //// // ////// ///// Q//// R01///////= +METAR BIAR 290000Z /////KT //// // ////// ///// Q//// R02/999491= +METAR BIAR 290000Z /////KT //// // ////// ///// Q//// R88/CLRD//= +METAR BIAR 290000Z /////KT //// // ////// ///// Q//// R99/CLRD//= +""" + bulletin = encoder.encode(test) + assert len(bulletin) == test.count('\n') - 1 + + # METAR BIAR 290000Z /////KT //// // ////// ///// Q//// R/SNOCLO= + + result = bulletin.pop() + assert result.get('translationFailedTAC') is None + + tree = ET.XML(ET.tostring(result)) + assert tree.find('%srunwayState' % iwxxm).get('nilReason') == des.NIL_SNOCLO_URL + + # METAR BIAR 290000Z /////KT //// // ////// ///// Q//// R/CLRD//= + + result = bulletin.pop() + assert result.get('translationFailedTAC') is None + + tree = ET.XML(ET.tostring(result)) + rs = tree.find('%srunwayState' % iwxxm) + assert rs[0].get('allRunways') == 'true' + assert rs[0].get('cleared') == 'true' + + # METAR BIAR 290000Z /////KT //// // ////// ///// Q//// R01///////= + + result = bulletin.pop() + assert result.get('translationFailedTAC') is None + + tree = ET.XML(ET.tostring(result)) + rs = tree.find('%srunwayState' % iwxxm) + assert rs[0].get('allRunways') == 'false' + assert rs.find('%sdesignator' % aixm).text == '01' + assert rs.find('%sdepositType' % iwxxm) is None + assert rs.find('%scontamination' % iwxxm) is None + assert rs.find('%sdepthOfDeposit' % iwxxm).get('nilReason') == nothingOfOperationalSignificance[0] + assert rs.find('%sestimatedSurfaceFrictionOrBrakingAction' % iwxxm) is None + + # METAR BIAR 290000Z /////KT //// // ////// ///// Q//// R02/999491= + + result = bulletin.pop() + assert result.get('translationFailedTAC') is None + + tree = ET.XML(ET.tostring(result)) + rs = tree.find('%srunwayState' % iwxxm) + assert rs[0].get('allRunways') == 'false' + assert rs.find('%sdesignator' % aixm).text == '02' + assert rs.find('%sdepositType' % iwxxm).get(xhref) == codes[des.RWYDEPST]['9'][0] + assert rs.find('%scontamination' % iwxxm).get(xhref) == codes[des.RWYCNTMS]['9'][0] + assert rs.find('%sdepthOfDeposit' % iwxxm).text == '200' + assert rs.find('%sdepthOfDeposit' % iwxxm).get('uom') == 'mm' + friction = rs.find('%sestimatedSurfaceFrictionOrBrakingAction' % iwxxm) + assert friction.get(xhref) == codes[des.RWYFRCTN]['91'][0] + + # METAR BIAR 290000Z /////KT //// // ////// ///// Q//// R88/CLRD//= + + result = bulletin.pop() + assert result.get('translationFailedTAC') is None + + tree = ET.XML(ET.tostring(result)) + rs = tree.find('%srunwayState' % iwxxm) + assert rs[0].get('allRunways') == 'true' + assert rs[0].get('cleared') == 'true' + + # METAR BIAR 290000Z /////KT //// // ////// ///// Q//// R99/CLRD//= + + result = bulletin.pop() + assert result.get('translationFailedTAC') is None + + tree = ET.XML(ET.tostring(result)) + rs = tree.find('%srunwayState' % iwxxm) + assert rs[0].get('fromPreviousReport') == 'true' + assert rs[0].get('cleared') == 'true' + + def test_trendTiming(): test = """SAXX99 XXXX 151200 @@ -1248,14 +1333,14 @@ def test_trendTiming(): def test_commonRunway(): test = """SAXX99 KXXX 151200 -METAR BIAR 290000Z /////MPS //// R01C/2000 ////// ///// Q//// WS R01C= +METAR BIAR 290000Z /////MPS //// R01C/2000 ////// ///// Q//// WS R01C R01C/999491= """ bulletin = encoder.encode(test) assert len(bulletin) == test.count('\n') - 1 tree = ET.XML(ET.tostring(bulletin.pop())) runways = tree.findall('%srunway' % iwxxm) - assert len(runways) == 2 + assert len(runways) == 3 # # First runway shall have the id that is shared with the rest runwayID = None @@ -1310,6 +1395,7 @@ def test_misc(): test_sky_conditions() test_windshears() test_seastates() + test_runwaystates() test_trendTiming() test_commonRunway() test_misc()