Unidata/MetPy

View on GitHub
src/metpy/plots/wx_symbols.py

Summary

Maintainability
A
35 mins
Test Coverage
# Copyright (c) 2016,2017,2019 MetPy Developers.
# Distributed under the terms of the BSD 3-Clause License.
# SPDX-License-Identifier: BSD-3-Clause
"""Simplify using the weather symbol font.

See WMO No.306 Attachment IV for more info on the symbols.
"""

import importlib.resources

import matplotlib.font_manager as fm
import numpy as np

from ..package_tools import Exporter

exporter = Exporter(globals())

# Create a matplotlib font object pointing to our weather symbol font
fontfile = importlib.resources.files('metpy.plots') / 'fonts/wx_symbols.ttf'
with importlib.resources.as_file(fontfile) as fname:
    # Need to pass str, not Path, for older matplotlib
    wx_symbol_font = fm.FontProperties(fname=str(fname))


@exporter.export
def wx_code_to_numeric(codes):
    """Determine the numeric weather symbol value from METAR code text.

    A robust method to identifies the numeric value for plotting the correct symbol from a
    decoded METAR current weather group. The METAR codes should be strings with no missing
    values or NaN strings (empty strings are okay).

    For example, if from a Pandas Dataframe ``sfc_df.wxcodes.fillna('')``.

    Parameters
    ----------
    codes : Sequence[str]
        String values of METAR weather codes

    Returns
    -------
    `numpy.ndarray`
        numeric codes of current weather symbols from the wx_code_map for use in plotting.
    """
    wx_sym_list = []
    for s in codes:
        wxcode = s.split()[0] if ' ' in s else s
        try:
            wx_sym_list.append(wx_code_map[wxcode])
        except KeyError:
            if wxcode[0].startswith(('-', '+')):
                options = [slice(None, 7), slice(None, 5), slice(1, 5), slice(None, 3),
                           slice(1, 3)]
            else:
                options = [slice(None, 6), slice(None, 4), slice(None, 2)]

            for opt in options:
                try:
                    wx_sym_list.append(wx_code_map[wxcode[opt]])
                    break
                except KeyError:
                    # That option didn't work--move on.
                    pass
            else:
                wx_sym_list.append(0)

    return np.array(wx_sym_list)


class CodePointMapping:
    """Map integer values to font code points."""

    def __init__(self, num, font_start, font_jumps=None, char_jumps=None):
        """Initialize the instance.

        Parameters
        ----------
        num : int
            The number of values that will be mapped
        font_start : int
            The first code point in the font to use in the mapping
        font_jumps : list[int, int], optional
            Sequence of code point jumps in the font. These are places where the next
            font code point does not correspond to a new input code. This is usually caused
            by there being multiple symbols for a single code. Defaults to :data:`None`, which
            indicates no jumps.
        char_jumps : list[int, int], optional
            Sequence of code jumps. These are places where the next code value does not
            have a valid code point in the font. This usually comes from place in the WMO
            table where codes have no symbol. Defaults to :data:`None`, which indicates no
            jumps.

        """
        next_font_jump = self._safe_pop(font_jumps)
        next_char_jump = self._safe_pop(char_jumps)
        font_point = font_start
        self.chrs = []
        code = 0
        while code < num:
            if next_char_jump and code >= next_char_jump[0]:
                jump_len = next_char_jump[1]
                code += jump_len
                self.chrs.extend([''] * jump_len)
                next_char_jump = self._safe_pop(char_jumps)
            else:
                self.chrs.append(chr(font_point))
                if next_font_jump and code >= next_font_jump[0]:
                    font_point += next_font_jump[1]
                    next_font_jump = self._safe_pop(font_jumps)
                code += 1
                font_point += 1

    @staticmethod
    def _safe_pop(lst):
        """Safely pop from a list.

        Returns None if list empty.

        """
        return lst.pop(0) if lst else None

    def __call__(self, code):
        """Return the Unicode code point corresponding to `code`.

        If code >= 1000, then an alternate code point is returned, with the thousands
        digit indicating which alternate.
        """
        if code < 1000:
            return self.chrs[code]
        else:
            alt = code // 1000
            code %= 1000
            return self.alt_char(code, alt)

    def __len__(self):
        """Return the number of codes supported by this mapping."""
        return len(self.chrs)

    def alt_char(self, code, alt):
        """Get one of the alternate code points for a given value.

        In the WMO tables, some code have multiple symbols. This allows getting that
        symbol rather than main one.

        Parameters
        ----------
        code : int
            The code for looking up the font code point
        alt : int
            The number of the alternate symbol

        Returns
        -------
        int
            The appropriate code point in the font

        """
        return chr(ord(self(code)) + alt)


#
# Set up mapping objects for various groups of symbols. The integer values follow from
# the WMO.
#

with exporter:
    #: Current weather -- codes 1xx are mapped into the automated station symbols
    current_weather = CodePointMapping(150, 0xE9A2, [(7, 2), (93, 2), (94, 2), (95, 2),
                                                     (97, 2), (103, -190)],
                                       [(0, 4), (100, 3), (106, 4), (113, 5), (119, 1),
                                        (136, 4)])

    #: Current weather from an automated station
    current_weather_auto = CodePointMapping(100, 0xE94F, [(92, 2), (95, 2)],
                                            [(0, 4), (6, 4), (13, 5), (19, 1), (36, 4),
                                             (49, 1), (59, 1), (69, 1), (79, 1), (88, 1),
                                             (97, 2)])

    #: Low clouds
    low_clouds = CodePointMapping(10, 0xE933, [(7, 1)], [(0, 1)])

    #: Mid-altitude clouds
    mid_clouds = CodePointMapping(10, 0xE93D, char_jumps=[(0, 1)])

    #: High clouds
    high_clouds = CodePointMapping(10, 0xE946, char_jumps=[(0, 1)])

    #: Sky cover symbols
    sky_cover = CodePointMapping(12, 0xE90A)

    #: Pressure tendency
    pressure_tendency = CodePointMapping(9, 0xE900)

    #####################################################################
    # This dictionary is for mapping METAR present weather text codes
    # to WMO codes for plotting wx symbols along with the station plots.
    # See Attachment IV of WMO No.306 for more information:
    # https://library.wmo.int/index.php?lvl=notice_display&id=13617
    # For unknown precipitation (UP), with thunderstorm this is mapped to 17, otherwise
    # it is mapped to 100 + automated station code

    wx_code_map = {'': 0, 'M': 0, 'TSNO': 0, 'VA': 4, 'FU': 4,
                   'HZ': 5, 'DU': 6, 'BLDU': 1007, 'SA': 1007,
                   'BLSA': 1007, 'VCBLSA': 1007, 'VCBLDU': 1007, 'BLPY': 1007,
                   'PO': 8, 'VCPO': 8, 'VCDS': 9, 'VCSS': 9,
                   'BR': 10, 'BCBR': 10, 'BC': 11, 'MIFG': 12,
                   'VCTS': 13, 'VIRGA': 14, 'VCSH': 16, 'TS': 17,
                   'THDR': 17, 'VCTSHZ': 17, 'TSFZFG': 17, 'TSBR': 17,
                   'TSDZ': 17, 'VCTSUP': 17, '-TSUP': 17, 'TSUP': 17, '+TSUP': 17,
                   'SQ': 18, 'FC': 19, '+FC': 19,
                   'DS': 31, 'SS': 31, 'DRSA': 31, 'DRDU': 31,
                   '+DS': 34, '+SS': 34, 'DRSN': 36, '+DRSN': 37,
                   '-BLSN': 38, 'BLSN': 38, '+BLSN': 39, 'VCBLSN': 38,
                   'VCFG': 40, 'BCFG': 41, 'PRFG': 44,
                   'FG': 45, 'FZFG': 49, '-VCTSDZ': 51, '-DZ': 51,
                   '-DZBR': 51, 'VCTSDZ': 53, 'DZ': 53, '+VCTSDZ': 55,
                   '+DZ': 55, '-FZDZ': 56, '-FZDZSN': 56, 'FZDZ': 57,
                   '+FZDZ': 57, 'FZDZSN': 57, '-DZRA': 58, 'DZRA': 59,
                   '+DZRA': 59, '-RA': 61, '-RABR': 61, 'RA': 63, 'RABR': 63, 'RAFG': 63,
                   'VCRA': 63, '+RA': 65, '-FZRA': 66, '-FZRASN': 66,
                   '-FZRABR': 66, '-FZRAPL': 66, '-FZRASNPL': 66, 'TSFZRAPL': 67,
                   '-TSFZRA': 67, 'FZRA': 67, '+FZRA': 67, 'FZRASN': 67,
                   'TSFZRA': 67, '-DZSN': 68, '-RASN': 68, '-SNRA': 68,
                   '-SNDZ': 68, 'RASN': 69, '+RASN': 69, 'SNRA': 69,
                   'DZSN': 69, 'SNDZ': 69, '+DZSN': 69, '+SNDZ': 69,
                   '-SN': 71, '-SNBR': 71, 'SN': 73, '+SN': 75,
                   '-SNSG': 77, 'SG': 77, '-SG': 77, 'IC': 78,
                   '-FZDZPL': 79, '-FZDZPLSN': 79, 'FZDZPL': 79, '-FZRAPLSN': 79,
                   'FZRAPL': 79, '+FZRAPL': 79, '-RAPL': 79, '-RASNPL': 79,
                   '-RAPLSN': 79, '+RAPL': 79, 'RAPL': 79, '-SNPL': 79,
                   'SNPL': 79, '-PL': 79, 'PL': 79, '-PLSN': 79,
                   '-PLRA': 79, 'PLRA': 79, '-PLDZ': 79, '+PL': 79,
                   'PLSN': 79, 'PLUP': 79, '+PLSN': 79, '-SH': 80,
                   '-SHRA': 80, 'SH': 81, 'SHRA': 81, '+SH': 81,
                   '+SHRA': 81, '-SHRASN': 83, '-SHSNRA': 83, '+SHRABR': 84,
                   'SHRASN': 84, '+SHRASN': 84, 'SHSNRA': 84, '+SHSNRA': 84,
                   '-SHSN': 85, 'SHSN': 86, '+SHSN': 86, '-GS': 87,
                   '-SHGS': 87, 'FZRAPLGS': 88, '-SNGS': 88, 'GSPLSN': 88,
                   'GSPL': 88, 'PLGSSN': 88, 'GS': 88, 'SHGS': 88,
                   '+GS': 88, '+SHGS': 88, '-GR': 89, '-SHGR': 89,
                   '-SNGR': 90, 'GR': 90, 'SHGR': 90, '+GR': 90, '+SHGR': 90,
                   '-TSRASN': 95, 'TSRASN': 95, '-TSSNRA': 95, 'TSSNRA': 95, '-VCTSRA': 1095,
                   '-TSRA': 1095, 'TSRA': 1095, '-TSDZ': 1095, 'VCTSRA': 1095,
                   'TSPL': 2095, '-TSSN': 2095, '-TSPL': 2095, 'TSSN': 2095, '-VCTSSN': 2095,
                   'VCTSSN': 2095, 'TSPLSN': 2095, 'TSSNPL': 2095, '-TSSNPL': 2095,
                   '-TSRAGR': 96, 'TSRAGS': 96, 'TSRAGR': 96, 'TSGS': 96, 'TSGR': 96,
                   '+TSFZRAPL': 97, '+VCTSRA': 1097, '+TSRA': 1097, '+TSFZRA': 1097,
                   '+TSSN': 2097, '+TSPL': 2097, '+TSPLSN': 2097, '+VCTSSN': 2097,
                   'TSSA': 98, 'TSDS': 98, 'TSDU': 98,
                   '+TSGS': 99, '+TSGR': 99, '+TSRAGS': 99, '+TSRAGR': 99,
                   'IN': 141, '-UP': 141, 'UP': 141, '+UP': 142,
                   '-FZUP': 147, 'FZUP': 147, '+FZUP': 148
                   }