From 68dfb1e983a8eabec89df8cfa80a2df88b01505e Mon Sep 17 00:00:00 2001 From: Nathan Wendt Date: Fri, 12 Jul 2024 23:23:15 -0500 Subject: [PATCH] Finish VGF reader and tests --- .codecov.yaml | 18 + .coveragerc | 8 + .github/workflows/testing.yml | 35 +- ci/linting_requirements.txt | 17 +- ci/test_requirements.txt | 3 +- examples/gempakio_examples.py | 7 +- pyproject.toml | 13 +- setup.cfg | 1 + src/gempakio/__init__.py | 1 + src/gempakio/decode/vgf.py | 3857 +++++++++++++++++++++++++++++++++ tests/data/fills.vgf | Bin 0 -> 1132 bytes tests/data/fronts.vgf | Bin 0 -> 1944 bytes tests/data/jet.vgf | Bin 0 -> 24500 bytes tests/data/lines.vgf | Bin 0 -> 4004 bytes tests/data/misc.vgf | Bin 0 -> 956 bytes tests/data/sig_airmet.vgf | Bin 0 -> 1240 bytes tests/data/sig_ccf.vgf | Bin 0 -> 1904 bytes tests/data/sig_intnl.vgf | Bin 0 -> 1248 bytes tests/data/sig_nonconv.vgf | Bin 0 -> 1240 bytes tests/data/symbols.vgf | Bin 0 -> 3008 bytes tests/data/tca.vgf | Bin 0 -> 833 bytes tests/data/text.vgf | Bin 0 -> 1474 bytes tests/data/tracks.vgf | Bin 0 -> 11624 bytes tests/data/volcano.vgf | Bin 0 -> 6860 bytes tests/data/watch.vgf | Bin 0 -> 11896 bytes tests/test_vgf.py | 616 ++++++ 26 files changed, 4549 insertions(+), 27 deletions(-) create mode 100644 .codecov.yaml create mode 100644 .coveragerc create mode 100644 src/gempakio/decode/vgf.py create mode 100644 tests/data/fills.vgf create mode 100644 tests/data/fronts.vgf create mode 100644 tests/data/jet.vgf create mode 100644 tests/data/lines.vgf create mode 100644 tests/data/misc.vgf create mode 100644 tests/data/sig_airmet.vgf create mode 100644 tests/data/sig_ccf.vgf create mode 100644 tests/data/sig_intnl.vgf create mode 100644 tests/data/sig_nonconv.vgf create mode 100644 tests/data/symbols.vgf create mode 100644 tests/data/tca.vgf create mode 100644 tests/data/text.vgf create mode 100644 tests/data/tracks.vgf create mode 100644 tests/data/volcano.vgf create mode 100644 tests/data/watch.vgf create mode 100644 tests/test_vgf.py diff --git a/.codecov.yaml b/.codecov.yaml new file mode 100644 index 0000000..8320c82 --- /dev/null +++ b/.codecov.yaml @@ -0,0 +1,18 @@ +coverage: + status: + patch: + default: + target: '80' + project: + library: + target: auto + threshold: 0.1% + paths: + - "src/gempakio/.*" + + tests: + target: 100% + paths: + - "tests/.*" + +comment: off \ No newline at end of file diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..e187cd9 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,8 @@ +[paths] +source = + src/ + /*/site-packages + +[run] +source = tests +source_pkgs = gempakio \ No newline at end of file diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 5825d68..2d17007 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -26,12 +26,39 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r ci/requirements.txt -r ci/test_requirements.txt + python -m pip install --upgrade setuptools + python -m pip install -r ci/requirements.txt -r ci/test_requirements.txt - name: Install gempakIO run: | - python setup.py install - + python -m pip install . + - name: Test with pytest run: | - pytest \ No newline at end of file + python -m pytest --cov-report json --cov src/gempakio tests + + - name: Upload coverage artifact + uses: actions/upload-artifact@v4 + with: + name: pypi-${{ matrix.python-version }}-${{ runner.os }} + path: coverage.json + retention-days: 1 + + codecov: + name: Codecov Upload + runs-on: ubuntu-latest + needs: build + timeout-minutes: 2 + steps: + - name: Checkout source + uses: actions/checkout@v4 + + - name: Download coverage artifacts + uses: actions/download-artifact@v4 + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v4 + with: + name: ${{ github.workflow }} + fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/ci/linting_requirements.txt b/ci/linting_requirements.txt index 9a4e235..d6ef0ab 100644 --- a/ci/linting_requirements.txt +++ b/ci/linting_requirements.txt @@ -1,21 +1,10 @@ -flake8>=5.0.4 +flake8 +ruff pycodestyle pyflakes -ruff - -flake8-bugbear -flake8-builtins -flake8-comprehensions flake8-continuation flake8-copyright flake8-isort isort -flake8-mutable -flake8-pie -flake8-print -flake8-quotes flake8-requirements -flake8-simplify - -flake8-docstrings -pydocstyle +pydocstyle \ No newline at end of file diff --git a/ci/test_requirements.txt b/ci/test_requirements.txt index 55b033e..cffeec6 100644 --- a/ci/test_requirements.txt +++ b/ci/test_requirements.txt @@ -1 +1,2 @@ -pytest \ No newline at end of file +pytest +pytest-cov \ No newline at end of file diff --git a/examples/gempakio_examples.py b/examples/gempakio_examples.py index bc30d02..701882d 100644 --- a/examples/gempakio_examples.py +++ b/examples/gempakio_examples.py @@ -10,11 +10,11 @@ # %% [markdown] # ### Imports +from cartopy import feature # %% import cartopy.crs as ccrs -from cartopy import feature -import matplotlib.pyplot as plt from matplotlib import colors +import matplotlib.pyplot as plt import metpy.calc as mpcalc from metpy.plots import Hodograph, SkewT, StationPlot from metpy.units import units @@ -22,7 +22,8 @@ import numpy as np import pyproj -from gempakio import GempakGrid, GempakSounding, GempakSurface, GridFile, SoundingFile, SurfaceFile +from gempakio import (GempakGrid, GempakSounding, GempakSurface, GridFile, SoundingFile, + SurfaceFile) # %% [markdown] # ### Misc. diff --git a/pyproject.toml b/pyproject.toml index d3cab79..6ae0098 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ lint = [ ] test = [ 'pytest', + 'pytest-cov' ] [build-system] @@ -55,12 +56,14 @@ combine_star = true [tool.ruff] line-length = 95 exclude = ['examples'] + +[tool.ruff.lint] select = ['A', 'B', 'C', 'CPY001', 'D', 'E', 'E226', 'F', 'G', 'I', 'N', 'NPY', 'Q', 'R', 'S', 'SIM', 'T', 'U', 'W'] ignore = ['F405', 'I001', 'RET504', 'RET505', 'RET506', 'RET507', 'RUF100'] preview = true explicit-preview-rules = true -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] 'src/gempakio/__init__.py' = ['F401'] 'tests/*.py' = ['S101'] @@ -68,11 +71,11 @@ explicit-preview-rules = true notice-rgx = '(?i)Copyright\s+(\(C\)\s+)?\d{4}' author = 'Nathan Wendt' -[tool.ruff.flake8-quotes] +[tool.ruff.lint.flake8-quotes] inline-quotes = 'single' multiline-quotes = 'double' -[tool.ruff.isort] +[tool.ruff.lint.isort] known-first-party = ['gempakio'] force-single-line = false relative-imports-order = 'closest-to-furthest' @@ -80,10 +83,10 @@ force-sort-within-sections = true order-by-type = false combine-as-imports = true -[tool.ruff.mccabe] +[tool.ruff.lint.mccabe] max-complexity = 61 -[tool.ruff.pydocstyle] +[tool.ruff.lint.pydocstyle] convention = 'numpy' [tool.setuptools] diff --git a/setup.cfg b/setup.cfg index 11325f6..4a4058d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -41,6 +41,7 @@ line = pydocstyle test = pytest + pytest-cov [pycodestyle] ignore = W503 diff --git a/src/gempakio/__init__.py b/src/gempakio/__init__.py index 9aee53c..4597d5f 100644 --- a/src/gempakio/__init__.py +++ b/src/gempakio/__init__.py @@ -4,6 +4,7 @@ """gempakIO.""" from gempakio.decode.gempak import GempakGrid, GempakSounding, GempakSurface +from gempakio.decode.vgf import VectorGraphicFile from gempakio.encode.gempak import GridFile, SoundingFile, SurfaceFile __version__ = '1.0.2' diff --git a/src/gempakio/decode/vgf.py b/src/gempakio/decode/vgf.py new file mode 100644 index 0000000..59d212c --- /dev/null +++ b/src/gempakio/decode/vgf.py @@ -0,0 +1,3857 @@ +# Copyright (c) 2024 Nathan Wendt. +# Distributed under the terms of the BSD 3-Clause License. +# SPDX-License-Identifier: BSD-3-Clause +"""Classes for decoding GEMPAK VGF files.""" + +import contextlib +from dataclasses import dataclass +from datetime import datetime, timezone +from enum import Enum +import json +import logging +import re +import sys + +import numpy as np + +from gempakio.tools import IOBuffer, NamedStruct + +logger = logging.getLogger(__name__) + +LIST_MEMBER_SIZE = 9 +MAX_ASH = 50 +MAX_COUNTIES = 400 +MAX_JET_POINTS = 50 +MAX_POINTS = 500 +MAX_SGWX_POINTS = 256 +MAX_SIGMET = 100 +MAX_TRACKS = 50 +TRACK_DT_SIZE = 18 +VGF_HEADER_SIZE = 40 + + +class VGClass(Enum): + """Values for vg_class from drwids.h.""" + + header = 0 + fronts = 1 + watches = 2 + lines = 3 + symbols = 4 + text = 5 + winds = 6 + every = 7 + comsym = 8 + products = 9 + tracks = 10 + sigmets = 11 + circle = 12 + marker = 13 + lists = 14 + met = 15 + blank = 16 + + +class VGType(Enum): + """Values for vg_type from vgstruct.h.""" + + line = 1 + front = 2 + circle = 4 + weather_symbol = 5 + watch_box = 6 + watch_county = 7 + wind_barb = 8 + wind_arrow = 9 + cloud_symbol = 10 + icing_symbol = 11 + pressure_tendency_symbol = 12 + past_weather_symbol = 13 + sky_cover = 14 + special_symbol = 15 + turbulence_symbol = 16 + text = 17 + justified_text = 18 + marker = 19 + special_line = 20 + special_text = 21 + file_header = 22 + directional_arrow = 23 + hash_mark = 24 + combination_weather_symbol = 25 + storm_track = 26 + international_sigmet = 27 + nonconvective_sigmet = 28 + convective_sigmet = 29 + convective_outlook = 30 + airmet = 31 + ccf = 32 + watch_status = 33 + lists = 34 + volcano = 35 + ash_cloud = 36 + jet = 37 + gfa = 38 + tca = 39 + tc_error_cone = 40 + tc_track = 41 + tc_break_point = 42 + sgwx = 43 + + +class LineType(Enum): + """Values for lintyp from GEMPAK.""" + + dotted = 0 + solid = 1 + short_dashed = 2 + medium_dashed = 3 + long_dash_short_dash = 4 + long_dash = 5 + long_dash_three_short_dash = 6 + long_dash_dot = 7 + long_dash_three_dot = 8 + extra_long_dash_two_dot = 9 + + +class SpecialLineType(Enum): + """Values for spltyp from settings.tbl.""" + + ball_chain = 1 + zigzag = 2 + scallop = 3 + pointed_arrow = 4 + alt_angle_ticks = 5 + filled_arrow = 6 + box_circles = 7 + two_x = 8 + filled_circles = 9 + line_fill_circle_line = 10 + tick_marks = 11 + line_x_line = 12 + fancy_arrow = 13 + fill_circle_x = 14 + box_x = 15 + line_circle_line = 16 + line_caret_line1 = 17 + line_caret_line2 = 18 + sine_curve = 19 + arrow_dashed = 20 + fill_arrow_dash = 21 + streamline = 22 + double_line = 23 + kink_line1 = 24 + kink_line2 = 25 + z_line = 26 + + +class MarkerType(Enum): + """Values for mrktyp from GEMPAK.""" + + none = 0 + plus_sign = 1 + octagon = 2 + triangle = 3 + box = 4 + small_x = 5 + diamond = 6 + up_arrow = 7 + x_bar = 8 + z_bar = 9 + y = 10 + box_diagonals = 11 + asterisk = 12 + hourglass = 13 + star = 14 + dot = 15 + large_x = 16 + filled_octagon = 17 + filled_triangle = 18 + filled_box = 19 + filled_diamond = 20 + filled_star = 21 + minus_sign = 22 + tropical_storm = 23 + hurricane = 24 + + +class ListType(Enum): + """Values for list types.""" + + county = 1 + zone = 2 + wfo = 3 + state = 4 + marine_county = 5 + + +class FrontType(Enum): + """Values for front types.""" + + stationary = 0 + stationary_aloft = 1 + warm = 2 + warm_aloft = 3 + cold = 4 + cold_aloft = 5 + occluded = 6 + dryline = 7 + intertropical = 8 + convergence = 9 + + +class FrontIntensity(Enum): + """Values for front intensity.""" + + unspecified = 0 + weak_decreasing = 1 + weak = 2 + weak_increasing = 3 + moderate_decreasing = 4 + moderate = 5 + moderate_increasing = 6 + strong_decreasing = 7 + strong = 8 + strong_increasing = 9 + + +class FrontCharacter(Enum): + """Values for front character.""" + + unspecified = 0 + frontal_decreasing = 1 + little_change = 2 + area_increasing = 3 + intertropical = 4 + forming_suspected = 5 + quasi_stationary = 6 + with_waves = 7 + diffuse = 8 + position_doubtful = 9 + + +class Basin(Enum): + """Values for TCA basin.""" + + atlantic = 0 + east_pacific = 1 + central_pacific = 2 + west_pacific = 3 + + +class Severity(Enum): + """Values for TCA severity.""" + + tropical_storm = 0 + hurricane = 1 + + +class StormType(Enum): + """Values for TCA storm type.""" + + hurricane = 0 + tropical_storm = 1 + tropical_depression = 2 + subtropical_storm = 3 + subtropical_depression = 4 + + +class AdvisoryType(Enum): + """Values for TCA advisory type.""" + + watch = 0 + warning = 1 + + +class SpecialGeography(Enum): + """Values for TCA special geography type.""" + + no_types = 0 + islands = 1 + water = 2 + + +class TropicalWatchWarningLevel(Enum): + """Values for watch-warning level.""" + + hurricane_warning = 0 + hurricane_watch = 1 + tropical_storm_warning = 2 + tropical_storm_watch = 3 + + +class WatchType(Enum): + """Values for watch type.""" + + tornado = 2 + severe_thunderstorm = 6 + + +@dataclass +class VectorGraphicAttribute: + """Vector graphic attribute base class.""" + + def __repr__(self): + """Return repr(self).""" + return (f'{type(self).__qualname__}' + f'({", ".join([f"{k}={v}" for k, v in vars(self).items()])})') + + +class BarbAttribute(VectorGraphicAttribute): + """Barb attribute.""" + + def __init__(self, wind_color, number_wind, width, size, wind_type, head_size, speed, + direction, lat, lon, flight_level_color, text_rotation, text_size, text_type, + turbulence_symbol, font, text_flag, text_width, text_color, line_color, + fill_color, align, text_lat, text_lon, offset_x, offset_y, text): + """Create wind barb attribute. + + Parameters + ---------- + wind_color : int + + number_wind : int + + width : int + + size : float + + wind_type : int + + head_size : float + + speed : float + + direction : float + + lat : float + + lon : float + + flight_level_color : int + + text_rotation : float + + text_size : float + + text_type : int + + turbulence_symbol : int + + font : int + + text_flag : int + + text_width : int + + text_color : int + + line_color : int + + fill_color : int + + align : int + + text_lat : float + + text_lon : float + + offset_x : int + + offset_y : int + + text : str + """ + self.wind_color = wind_color + self.number_wind = number_wind + self.width = width + self.size = size + self.wind_type = wind_type + self.head_size = head_size + self.speed = speed + self.direction = direction + self.lat = lat + self.lon = lon + self.flight_level_color = flight_level_color + self.text_rotation = text_rotation + self.text_size = text_size + self.text_type = text_type + self.turbulence_symbol = turbulence_symbol + self.font = font + self.text_flag = text_flag + self.text_width = text_width + self.text_color = text_color + self.line_color = line_color + self.fill_color = fill_color + self.align = align + self.text_lat = text_lat + self.text_lon = text_lon + self.offset_x = offset_x + self.offset_y = offset_y + self.text = text + + +class BreakPointAttribute(VectorGraphicAttribute): + """Break point attribute.""" + + def __init__(self, lat, lon, name): + """Create break point attribute. + + Parameters + ---------- + lat : float + + lon : float + + name : str + """ + self.lat = lat + self.lon = lon + self.name = name + + +class TrackAttribute(VectorGraphicAttribute): + """Tropical cylcone track attribute.""" + + def __init__(self, advisory_date, tau, max_wind, wind_gust, minimum_pressure, + development_level, development_label, direction, speed, date_label, + storm_source, lat, lon): + """Create tropical cyclone track attribute. + + Parameters + ---------- + advisory_date : str + + tau : str + + max_wind : str + + wind_gust : str + + minimum_pressure : str + + development_level : str + + development_label : str + + direction : str + + speed : str + + date_label : str + + storm_source : str + + lat : float + + lon : float + """ + self.advisory_date = advisory_date + self.tau = tau + self.max_wind = max_wind + self.wind_gust = wind_gust + self.minimum_pressure = minimum_pressure + self.development_level = development_level + self.development_label = development_label + self.direction = direction + self.speed = speed + self.date_label = date_label + self.storm_source = storm_source + self.lat = lat + self.lon = lon + + +class HashAttribute(VectorGraphicAttribute): + """Hash attribute.""" + + def __init__(self, wind_color, number_wind, width, size, wind_type, + head_size, speed, direction, lat, lon): + """Create hash attribute. + + Parameters + ---------- + wind_color : int + + number_wind : int + + width : int + + size : float + + wind_type : int + + head_size : float + + speed : float + + direction : float + + lat : float + + lon : float + """ + self.wind_color = wind_color + self.number_wind = number_wind + self.width = width + self.size = size + self.wind_type = wind_type + self.head_size = head_size + self.speed = speed + self.direction = direction + self.lat = lat + self.lon = lon + + +class LineAttribute(VectorGraphicAttribute): + """Line attribute.""" + + def __init__(self, line_color, number_points, line_type, stroke, + direction, size, width, lat, lon): + """Create line attribute. + + Parameters + ---------- + line_color : int + + number_points : int + + line_type : int + + stroke : int + + direction : float + + size : float + + width : int + + lat : `numpy.ndarray` + + lon : `numpy.ndarray` + """ + self.line_color = line_color + self.number_points = number_points + self.line_type = line_type + self.stroke = stroke + self.direction = direction + self.size = size + self.width = width + self.lat = lat + self.lon = lon + + +@dataclass +class VectorGraphicElement: + """Base class for VGF elements.""" + + def __init__(self, header_struct): + """Vector graphic element. + + Parameters + ---------- + header_struct : `NamedStruct` + """ + self.delete = header_struct.delete + self.vg_type = header_struct.vg_type + self.vg_class = header_struct.vg_class + self.filled = header_struct.filled + self.closed = header_struct.closed + self.smooth = header_struct.smooth + self.version = header_struct.version + self.group_type = header_struct.group_type + self.group_number = header_struct.group_number + self.major_color = header_struct.major_color + self.minor_color = header_struct.minor_color + self.record_size = header_struct.record_size + self.min_lat = header_struct.min_lat + self.min_lon = header_struct.min_lon + self.max_lat = header_struct.max_lat + self.max_lon = header_struct.max_lon + + def __repr__(self): + """Return repr(self).""" + return (f'{type(self).__qualname__}' + f'[{VGClass(self.vg_class).name}, {VGType(self.vg_type).name}]') + + @property + def bounds(self): + """Get bounding box of element.""" + if (hasattr(self, 'lat') and hasattr(self, 'lon') + and len(np.atleast_1d(self.lat)) > 1): + xmin = min(self.lon) + xmax = max(self.lon) + ymin = min(self.lat) + ymax = max(self.lat) + + return (ymin, xmin, ymax, xmax) + else: + raise NotImplementedError(f'bounds undefined for {type(self).__qualname__}') + + +class AshCloudElement(VectorGraphicElement): + """Ash cloud element.""" + + def __init__(self, header_struct, subtype, number_points, distance, forecast_hour, + line_type, line_width, side_of_line, speed, speeds, direction, + flight_level1, flight_level2, rotation, text_size, text_type, + turbulence_symbol, font, text_flag, width, text_color, line_color, + fill_color, align, text_lat, text_lon, offset_x, offset_y, text, + lat, lon): + """Create ash cloud element. + + Parameters + ---------- + header_struct : `NamedStruct` + + subtype : int + Type of ash cloud (side of line, line, area). + + number_points : int + + distance : float + + forecast_hour : int + + line_type : int + + line_width : int + + side_of_line : int + + speed : float + + speeds : str + + direction : str + + flight_level1 : str + + flight_level2 : str + + rotation : float + + text_size : float + + text_type : int + + turbulence_symbol : int + + font : int + + text_flag : int + + text_width : int + + text_color : int + + line_color : int + + fill_color : int + + align : int + + text_lat : float + + text_lon : float + + offset_x : int + + offset_y : int + + text : str + + lat : `numpy.ndarray` + + lon : `numpy.ndarray` + """ + super().__init__(header_struct) + self.subtype = subtype + self.number_points = number_points + self.distance = distance + self.forecast_hour = forecast_hour + self.line_type = line_type + self.line_width = line_width + self.side_of_line = side_of_line + self.speed = speed + self.speeds = speeds + self.direction = direction + self.flight_level1 = flight_level1 + self.flight_level2 = flight_level2 + self.rotation = rotation + self.text_size = text_size + self.text_type = text_type + self.turbulence_symbol = turbulence_symbol + self.font = font + self.text_flag = text_flag + self.width = width + self.text_color = text_color + self.line_color = line_color + self.fill_color = fill_color + self.align = align + self. text_lat = text_lat + self.text_lon = text_lon + self.offset_x = offset_x + self.offset_y = offset_y + self.text = text + self.lat = lat + self.lon = lon + + +class CircleElement(VectorGraphicElement): + """Circle element.""" + + def __init__(self, header_struct, number_points, line_type, + line_type_hardware, width, line_width_hardware, + lat, lon): + """Create circle element. + + Parameters + ---------- + header_struct : `NamedStruct` + + number_points : int + + line_type : int + Integer code defining line type. See Appendix C in + GEMPAK documentation for details. + + line_type_hardware : int + + width : int + + line_width_hardware : int + + lat : `numpy.ndarray` + + lon : `numpy.ndarray` + """ + super().__init__(header_struct) + self.number_points = number_points + self.line_type = line_type + self.line_type_hardware = line_type_hardware + self.width = width + self.line_width_hardware = line_width_hardware + self.lat = lat + self.lon = lon + + self._set_properties() + + def _set_properties(self): + """Decode line type number to set properties.""" + code = f'{self.line_type:02d}' + self.line_modifier = int(code[0]) + self.line_style = LineType(int(code[1])).name + + +class CollaborativeConvectiveForecastElement(VectorGraphicElement): + """CCF element.""" + + def __init__(self, header_struct, subtype, number_points, coverage, storm_tops, + probability, growth, speed, direction, text_lat, text_lon, arrow_lat, + arrow_lon, high_fill, med_fill, low_fill, line_type, arrow_size, + rotation, text_size, text_type, turbulence_symbol, font, text_flag, width, + fill_color, align, offset_x, offset_y, text, text_layout, lat, lon): + """Create CCF element. + + Parameters + ---------- + header_struct : `NamedStruct` + + subtype : int + + number_points : int + + coverage : int + + storm_tops : int + + probabiltiy : int + + growth : int + + speed : float + + direction : float + + text_lat : float + + text_lon : float + + arrow_lat : float + + arrow_lon : float + + high_fill : int + + med_fill : int + + low_fill : int + + line_type : int + + arrow_size : float + + rotation : float + + text_size : float + + text_type : int + + turbulence_symbol : int + + font : int + + text_flag : int + + text_width : int + + text_color : int + + line_color : int + + fill_color : int + + align : int + + text_lat : float + + text_lon : float + + offset_x : int + + offset_y : int + + text : str + + lat : `numpy.ndarray` + + lon : `numpy.ndarray` + """ + super().__init__(header_struct) + self.subtype = subtype + self.number_points = number_points + self.coverage = coverage + self.storm_tops = storm_tops + self.probability = probability + self.growth = growth + self.speed = speed + self.direction = direction + self.text_lat = text_lat + self.text_lon = text_lon + self.arrow_lat = arrow_lat + self.arrow_lon = arrow_lon + self.high_fill = high_fill + self.med_fill = med_fill + self.low_fill = low_fill + self.line_type = line_type + self.arrow_size = arrow_size + self.rotation = rotation + self.text_size = text_size + self.text_type = text_type + self.turbulence_symbol = turbulence_symbol + self.font = font + self.text_flag = text_flag + self.width = width + self.fill_color = fill_color + self.align = align + self.offset_x = offset_x + self.offset_y = offset_y + self.text = text + self.text_layout = text_layout + self.lat = lat + self.lon = lon + + +class FileHeaderElement(VectorGraphicElement): + """File header element.""" + + def __init__(self, header_struct, version, notes): + """Create file header element. + + Parameters + ---------- + header_struct : `NamedStruct` + + version : str + + notes : str + """ + super().__init__(header_struct) + self.gempak_version = version + self.notes = notes + + +class FrontElement(VectorGraphicElement): + """Front element.""" + + def __init__(self, header_struct, number_points, front_code, pip_size, + pip_stroke, pip_direction, width, label, lat, lon): + """Create front element. + + Parameters + ---------- + header_struct : `NamedStruct` + + number_points : int + + front_code : int + Integer code defining the front type, intensity, and character. + See Appendix C in GEMPAK documentation for details. + + pip_size : int + Size of barbs, scallops, etc. + + pip_stroke : int + GEMPAK color code. + + pip_direction : int + Direction pips are facing. Right of line (1) or left of line (-1). + + width : int + Width of front line. + + label : str + Front label. If present, typically will be string front code. + + lat : `numpy.ndarray` + + lon : `numpy.ndarray` + """ + super().__init__(header_struct) + self.number_points = number_points + self.front_code = front_code + self.pip_size = pip_size + self.pip_stroke = pip_stroke + self.pip_direction = pip_direction + self.width = width + self.label = label + self.lat = lat + self.lon = lon + + self._set_properties() + + def _set_properties(self): + """Decode front code to set properties.""" + code = f'{self.front_code:03d}' + self.front_type = FrontType(int(code[0])).name + self.front_intensity = FrontIntensity(int(code[1])).name + self.front_character = FrontCharacter(int(code[2])).name + + +class GraphicalForecastAreaElement(VectorGraphicElement): + """GFA element.""" + + def __init__(self, header_struct, number_blocks, number_points, blocks, + lat, lon): + """Create GFA element. + + Parameters + ---------- + header_struct : `NamedStruct` + + number_blocks : int + + number_points : int + + blocks : str + + lat : `numpy.ndarray` + + lon : `numpy.ndarray` + """ + super().__init__(header_struct) + self.number_blocks = number_blocks + self.number_points = number_points + self.blocks = blocks + self.lat = lat + self.lon = lon + + +class JetElement(VectorGraphicElement): + """Jet element.""" + + def __init__(self, header_struct, line_attribute, number_barbs, barb_attributes, + number_hashes, hash_attributes): + """Create jet element. + + Parameters + ---------- + header_struct : `NamedStruct` + + line_attribute : `LineAttribute` + + number_barbs : int + + barb_attributes : array_like of `BarbAttribute` + + number_hashes : int + + hash_attribute : array_like of `HashAttribute` + """ + super().__init__(header_struct) + self.line_attribute = line_attribute + self.number_barbs = number_barbs + self.barb_attributes = barb_attributes + self.number_hashes = number_hashes + self.hash_attributes = hash_attributes + + +class LineElement(VectorGraphicElement): + """Line element.""" + + def __init__(self, header_struct, number_points, line_type, + line_type_hardware, width, line_width_hardware, + lat, lon): + """Create line element. + + Parameters + ---------- + header_struct : `NamedStruct` + + number_points : int + + line_type : int + Integer code defining line type. See Appendix C in + GEMPAK documentation for details. + + line_type_hardware : int + + line_type_hardware : int + + width : int + + line_width_hardware : int + + lat : `numpy.ndarray` + + lon : `numpy.ndarray` + """ + super().__init__(header_struct) + self.number_points = number_points + self.line_type = line_type + self.line_type_hardware = line_type_hardware + self.width = width + self.line_width_hardware = line_width_hardware + self.lat = lat + self.lon = lon + + self._set_properties() + + def _set_properties(self): + """Decode line type number to set properties.""" + code = f'{self.line_type:02d}' + self.line_modifier = int(code[0]) + self.line_style = LineType(int(code[1])).name + + +class ListElement(VectorGraphicElement): + """List element.""" + + def __init__(self, header_struct, list_type, marker_type, + marker_size, marker_width, number_items, + list_items, lat, lon): + """Create list element. + + Parameters + ---------- + header_struct : `NamedStruct` + + list_type : int + County (1), zone (2), WFO (3), state (4), or marine (5). + + marker_type : int + Integer code defining the symbol used for the marker. See + Appendix C in GEMPAK documentation for details. + + marker_size : float + + number_items : int + + list_items : array_like of str + + lat : `numpy.ndarray` + Latitude of list item centroid. + + lon : `numpy.ndarray` + Longitude of list item centroid. + """ + super().__init__(header_struct) + self.list_type = list_type + self.marker_type = marker_type + self.marker_size = marker_size + self.marker_width = marker_width + self.number_items = number_items + self.list_items = list_items + self.lat = lat + self.lon = lon + + self._set_properties() + + def _set_properties(self): + """Set list properties using header information.""" + self.list_type_name = ListType(self.list_type).name + self.marker_name = MarkerType(self.marker_type).name + + +class SigmetElement(VectorGraphicElement): + """"SIGMET element.""" + + def __init__(self, header_struct, subtype, number_points, line_type, line_width, + side_of_line, area, flight_info_region, status, distance, message_id, + sequence_number, start_time, end_time, remarks, sonic, phenomena, + phenomena2, phenomena_name, phenomena_lat, phenomena_lon, pressure, + max_wind, free_text, trend, movement, type_indicator, type_time, + flight_level, speed, direction, tops, forecaster, lat, lon): + """Create SIGMET element. + + Parameters + ---------- + header_struct : `NamedStruct` + + subtype : int + Type of SIGMET area (side of line, line, area). + + number_points : int + + line_type : int + Integer code defining special line type. See Appendix C + in GEMPAK documentation for details. + + line_width : int + + side_of_line : int + + area : str + MWO inidicator of unit. + + flight_info_region : str + Location indicator of FIR unit. + + status : int + New (0), amend (1), or cancel (2). + + distance : float + + message_id : str + + sequence_number : int + + start_time : str + Format: ddHHMM. + + end_time : str + Format: ddHHMM. + + marker_type : int + Integer code defining the symbol used for the marker. See + Appendix C in GEMPAK documentation for details. + + marker_size : float + + number_items : int + + remarks : str + + sonic : int + Supersonic indicator (0 or 1). + + phenomena : str + + phenomena2 : str + + phenomena_name : str + + phenomena_lat : str + + phenomena_lon : str + + pressure : int + + max_wind : int + + free_text : str + + trend : str + + movement : str + + type_indicator : int + Obs or forecast. (0, 1, 2) + + type_time : str + Format: ddHHMM. + + flight_level : int + + speed : int + + direction : str + Direction of phenomena. + + tops : str + + forecaster : str + + lat : `numpy.ndarray` + Latitude of list item centroid. + + lon : `numpy.ndarray` + Longitude of list item centroid. + """ + super().__init__(header_struct) + self.subtype = subtype + self.number_points = number_points + self.line_type = line_type + self.line_width = line_width + self.side_of_line = side_of_line + self.area = area + self.flight_info_region = flight_info_region + self.status = status + self.distance = distance + self.message_id = message_id + self.sequence_number = sequence_number + self.start_time = start_time + self.end_time = end_time + self.remarks = remarks + self.sonic = sonic + self.phenomena = phenomena + self.phenomena2 = phenomena2 + self.phenomena_name = phenomena_name + self.phenomena_lat = phenomena_lat + self.phenomena_lon = phenomena_lon + self.pressure = pressure + self.max_wind = max_wind + self.free_text = free_text + self.trend = trend + self.movement = movement + self.type_indicator = type_indicator + self.type_time = type_time + self.flight_level = flight_level + self.speed = speed + self.direction = direction + self.tops = tops + self.forecaster = forecaster + self.lat = lat + self.lon = lon + + +class SignificantWeatherElement(VectorGraphicElement): + """SGWX element.""" + + def __init__(self, header_struct, subtype, number_points, text_lat, text_lon, + arrow_lat, arrow_lon, line_element, line_type, line_width, + arrow_size, special_symbol, weather_symbol, text_rotation, + text_size, text_type, turbulence_symbol, font, text_flag, + text_width, text_color, line_color, fill_color, text_align, + offset_x, offset_y, text, area_lat, area_lon): + """Create special line element. + + Parameters + ---------- + header_struct : `NamedStruct` + + subtype : int + + number_points : int + + text_lat : float + + text_lon : float + + arrow_lat : float + + arrow_lon : float + + line_element : int + + line_type : int + + line_width : int + + arrow_size : float + + special_symbol : int + + weather_symbol : int + + text_rotation : float + + rotation : float + Text rotation. + + size : float + GEMPAK font size code. See getSztext in cvgv2x.c. in + GEMPAK source for conversion to points. + + text_type : int + Integer code for the type of text (e.g., general, aviation). + cvgv2x.c in GEMPAK source contains more details. + + turbulence_symbol : int + Integer code for turbulence symbol. Used if the text_type is + in the aviation family. See Appendix C in GEMPAK documentation + for details on turbulence symbol codes. + + font : int + Integer code defining the font to use. See getFontStyle in cvgv2x.c + in GEMPAK source for details. + + text_flag : int + Flag for using hardware or software font. + + width : int + + text_color : int + GEMPAK color code for text. + + line_color : int + GEMPAK color code for lines (i.e., box, underline). + + fill_color : int + GEMPAK color code for text box fill. + + text_align : int + Integer code for text alignment. Center (0), left (-1), + or right (1). + + offset_x : int + Symbol offset in x direction. + + offset_y : int + Symbole offset in y direction. + + text : str + + area_lat : `numpy.ndarray` + + area_lon : `numpy.ndarray` + """ + super().__init__(header_struct) + self.subtype = subtype + self.number_points = number_points + self.text_lat = text_lat + self.text_lon = text_lon + self.arrow_lat = arrow_lat + self.arrow_lon = arrow_lon + self.line_element = line_element + self.line_type = line_type + self.line_width = line_width + self.arrow_size = arrow_size + self.special_symbol = special_symbol + self.weather_symbol = weather_symbol + self.text_rotation = text_rotation + self.text_size = text_size + self.text_type = text_type + self.turbulence_symbol = turbulence_symbol + self.font = font + self.text_flag = text_flag + self.text_width = text_width + self.text_color = text_color + self.line_color = line_color + self.fill_color = fill_color + self.text_align = text_align + self.offset_x = offset_x + self.offset_y = offset_y + self.text = text + self.area_lat = area_lat + self.area_lon = area_lon + + +class SpecialLineElement(VectorGraphicElement): + """Special line element.""" + + def __init__(self, header_struct, number_points, line_type, + stroke, direction, size, width, lat, lon): + """Create special line element. + + Parameters + ---------- + header_struct : `NamedStruct` + + number_points : int + + line_type : int + Integer code defining special line type. See Appendix C + in GEMPAK documentation for details. + + stroke : int + GEMPAK color code. + + direction : int + CW (1) or CCW (-1). + + size : float + Size of special line elements (e.g., dots, crosses, arrow heads, etc.). + + width : int + + lat : `numpy.ndarray` + + lon : `numpy.ndarray` + """ + super().__init__(header_struct) + self.number_points = number_points + self.line_type = line_type + self.stroke = stroke + self.direction = direction + self.size = size + self.width = width + self.lat = lat + self.lon = lon + + self._set_properties() + + def _set_properties(self): + """Set properties using special line type code.""" + self.line_style = SpecialLineType(self.line_type).name + + +class SpecialTextElement(VectorGraphicElement): + """Special text element.""" + + def __init__(self, header_struct, rotation, size, text_type, + turbulence_symbol, font, text_flag, width, text_color, + line_color, fill_color, align, lat, lon, offset_x, + offset_y, text): + """Create special text element. + + Parameters + ---------- + header_struct : `NamedStruct` + + rotation : float + Text rotation. + + size : float + GEMPAK font size code. See getSztext in cvgv2x.c. in + GEMPAK source for conversion to points. + + text_type : int + Integer code for the type of text (e.g., general, aviation). + cvgv2x.c in GEMPAK source contains more details. + + turbulence_symbol : int + Integer code for turbulence symbol. Used if the text_type is + in the aviation family. See Appendix C in GEMPAK documentation + for details on turbulence symbol codes. + + font : int + Integer code defining the font to use. See getFontStyle in cvgv2x.c + in GEMPAK source for details. + + text_flag : int + Flag for using hardware or software font. + + width : int + + text_color : int + GEMPAK color code for text. + + line_color : int + GEMPAK color code for lines (i.e., box, underline). + + fill_color : int + GEMPAK color code for text box fill. + + align : int + Integer code for text alignment. Center (0), left (-1), + or right (1). + + lat : `numpy.ndarray` + + lon : `numpy.ndarray` + + offset_x : int + Symbol offset in x direction. + + offset_y : int + Symbole offset in y direction. + + text : str + """ + super().__init__(header_struct) + self.rotation = rotation + self.size = size + self.text_type = text_type + self.turbulence_symbol = turbulence_symbol + self.font = font + self.text_flag = text_flag + self.width = width + self.text_color = text_color + self.line_color = line_color + self.fill_color = fill_color + self.align = align + self.lat = lat + self.lon = lon + self.offset_x = offset_x + self.offset_y = offset_y + self.text = text + + +class SymbolElement(VectorGraphicElement): + """Symbol element.""" + + def __init__(self, header_struct, number_symbols, width, size, + symbol_type, symbol_code, offset_x, offset_y, lat, lon): + """Create symbol element. + + Parameters + ---------- + header_struct : `NamedStruct` + + number_symbols : int + + width : int + + size : float + + symbol_type : int + Not filled (1) or filled (2). + + symbol_code : int + Integer code defining the symbole. See Appendix C in + GEMPAK documentation for details. + + offset_x : int + Symbol offset in x direction. + + offset_y : int + Symbole offset in y direction. + + lat : `numpy.ndarray` + + lon : `numpy.ndarray` + """ + super().__init__(header_struct) + self.number_symbols = number_symbols + self.width = width + self.size = size + self.symbol_type = symbol_type + self.symbol_code = symbol_code + self.lat = lat + self.lon = lon + self.offset_x = offset_x + self.offset_y = offset_y + + +class TextElement(VectorGraphicElement): + """Text element.""" + + def __init__(self, header_struct, rotation, size, font, + text_flag, width, align, lat, lon, offset_x, + offset_y, text): + """Create text element. + + Parameters + ---------- + header_struct : `NamedStruct` + + rotation : float + Text rotation. + + size : float + GEMPAK font size code. See getSztext in cvgv2x.c. in + GEMPAK source for conversion to points. + + font : int + Integer code defining the font to use. See getFontStyle in cvgv2x.c + in GEMPAK source for details. + + text_flag : int + Flag for using hardware or software font. + + width : int + + align : int + Integer code for text alignment. Center (0), left (-1), + or right (1). + + lat : `numpy.ndarray` + + lon : `numpy.ndarray` + + offset_x : int + Symbol offset in x direction. + + offset_y : int + Symbole offset in y direction. + """ + super().__init__(header_struct) + self.rotation = rotation + self.size = size + self.font = font + self.text_flag = text_flag + self.width = width + self.align = align + self.lat = lat + self.lon = lon + self.offset_x = offset_x + self.offset_y = offset_y + self.text = text + + +class TrackElement(VectorGraphicElement): + """Storm track element.""" + + def __init__(self, header_struct, track_type, total_points, initial_points, + initial_line_type, extrapolated_line_type, initial_mark_type, + extrapolated_mark_type, line_width, speed, direction, increment, + skip, font, font_flag, font_size, times, lat, lon): + """Create storm track element. + + Parameters + ---------- + header_struct : `NamedStruct` + + track_type : int + + total_points : int + + initial_points : int + Number of points from user to calculate track. + + initial_line_type : int + Integer code for line type of initial points. See Appendix C + in GEMPAK documentation for details. + + extrapolated_line_type : int + Integer code for line type of extrapolated points. See + Appendix C in GEMPAK documentation for details. + + initial_mark_type : int + Integer code for markers of initial points. See Appendix C + in GEMPAK documentation for details. + + extrapolated_mark_type : int + Integer code for markers of extrapolated points. See + Appendix C in GEMPAK documentation for details. + + line_width : int + + speed : float + Storm speed in knots between last two initial points. + + direction : float + Storm direction in degrees between last two initial points. + + increment : int + Increment (in minutes) between extrapolated points. + + skip : int + Skip factor for extrapolated point labels. + + font : int + Integer code defining the font to use. See getFontStyle in cvgv2x.c + in GEMPAK source for details. + + font_flag : int + Flag for using hardware or software font. + + font_size : float + GEMPAK font size code. See getSztext in cvgv2x.c. in + GEMPAK source for conversion to points. + + times : array_like of str + Array of time labels for points. + + lat : `numpy.ndarray` + + lon : `numpy.ndarray` + """ + super().__init__(header_struct) + self.track_type = track_type + self.total_points = total_points + self.initial_points = initial_points + self.initial_line_type = initial_line_type + self.extrapolated_line_type = extrapolated_line_type + self.initial_mark_type = initial_mark_type + self.extrapolated_mark_type = extrapolated_mark_type + self.line_width = line_width + self.speed = speed + self.direction = direction + self.increment = increment + self.skip = skip + self.font = font + self.font_flag = font_flag + self.font_size = font_size + self.times = times + self.lat = lat + self.lon = lon + + +class TropicalCycloneBase(VectorGraphicElement): + """Base class for TC elements.""" + + def __init__(self, header_struct, storm_number, issue_status, basin, advisory_number, + storm_name, storm_type, valid_time, tz, forecast_period): + """TC base class. + + Parameters + ---------- + header_struct : `NamedStruct` + + storm_number : str + + issue_status : str + + basin : int + + advisory_number : str + + storm_name : str + + valid_time : str + + tz : str + + forecast_period : str + """ + super().__init__(header_struct) + self.storm_number = storm_number + self.issue_status = issue_status + self.basin = Basin(basin) + self.advisory_number = advisory_number + self.storm_name = storm_name + self.storm_type = StormType(storm_type) + self.valid_time = datetime.strptime( + valid_time, '%y%m%d/%H%M' + ).replace(tzinfo=timezone.utc) + self.timezone = tz + self.forecast_period = forecast_period + + +class TropicalCycloneAdvisoryElement(VectorGraphicElement): + """TCA element.""" + + def __init__(self, header_struct, storm_number, issue_status, basin, advisory_number, + storm_name, storm_type, valid_time, tz, text_lat, text_lon, + text_font, text_size, text_width, number_ww, ww): + """Create TCA element. + + Parameters + ---------- + header_struct : `NamedStruct` + + storm_number : str + + issue_status : str + + basin : int + + advisory_number : str + + storm_name : str + + valid_time : str + + tz : str + + text_lat : float + + text_lon : float + + text_font : int + + text_size : float + + text_width : int + + number_ww : int + + ww : array_like of dict + Array-like object containing dict of watches/warnings. + """ + super().__init__(header_struct) + self.storm_number = storm_number + self.issue_status = issue_status + self.basin = basin + self.advisory_number = advisory_number + self.storm_name = storm_name + self.storm_type = storm_type + self.valid_time = valid_time + self.timezone = tz + self.text_lat = text_lat + self.text_lon = text_lon + self.text_font = text_font + self.text_size = text_size + self.text_width = text_width + self.number_ww = number_ww + self.ww = ww + + +class TropicalCycloneBreakPointElement(TropicalCycloneBase): + """Tropical cyclone break point element.""" + + def __init__(self, header_struct, storm_number, issue_status, basin, advisory_number, + storm_name, storm_type, valid_time, tz, forecast_period, line_color, + line_width, ww_level, number_points, breakpoints): + """Create TC break point element. + + Parameters + ---------- + header_struct : `NamedStruct` + + storm_number : str + + issue_status : str + + basin : int + + advisory_number : str + + storm_name : str + + valid_time : str + + tz : str + + forecast_period : str + + line_color : int + + line_width : int + + ww_level : int + + number_points : int + + breakpoints : array_like of `BreakPointAttribute` + """ + super().__init__(header_struct, storm_number, issue_status, basin, advisory_number, + storm_name, storm_type, valid_time, tz, forecast_period) + self.line_color = line_color + self.line_width = line_width + self.ww_level = TropicalWatchWarningLevel(ww_level) + self.number_points = number_points + self.breakpoints = breakpoints + + +class TropicalCycloneErrorElement(TropicalCycloneBase): + """Tropical cyclone error cone element.""" + + def __init__(self, header_struct, storm_number, issue_status, basin, advisory_number, + storm_name, storm_type, valid_time, tz, forecast_period, line_color, + line_type, fill_color, fill_type, number_points, lat, lon): + """Create tropical cyclone error cone element. + + Parameters + ---------- + header_struct : `NamedStruct` + + storm_number : str + + issue_status : str + + basin : int + + advisory_number : str + + storm_name : str + + valid_time : str + + tz : str + + forecast_period : str + + line_color : int + + line_width : int + + ww_level : int + + number_points : int + + breakpoints : array_like of `BreakPointAttribute` + """ + super().__init__(header_struct, storm_number, issue_status, basin, advisory_number, + storm_name, storm_type, valid_time, tz, forecast_period) + self.forecast_period = forecast_period + self.line_color = line_color + self.line_type = line_type + self.fill_color = fill_color + self.fill_type = fill_type + self.number_points = number_points + self.lat = lat + self.lon = lon + + +class TropicalCycloneTrackElement(TropicalCycloneBase): + """Tropical cyclone track element.""" + + def __init__(self, header_struct, storm_number, issue_status, basin, advisory_number, + storm_name, storm_type, valid_time, tz, forecast_period, line_color, + line_type, number_points, track_points): + """Create tropical cyclone track element. + + Parameters + ---------- + header_struct : `NamedStruct` + + storm_number : str + + issue_status : str + + basin : int + + advisory_number : str + + storm_name : str + + valid_time : str + + tz : str + + forecast_period : str + + line_color : int + + line_width : int + + ww_level : int + + number_points : int + + breakpoints : array_like of `BreakPointAttribute` + """ + super().__init__(header_struct, storm_number, issue_status, basin, advisory_number, + storm_name, storm_type, valid_time, tz, forecast_period) + self.forecast_period = forecast_period + self.line_color = line_color + self.line_type = line_type + self.number_points = number_points + self.track_points = track_points + + +class VolcanoElement(VectorGraphicElement): + """Volcano element.""" + + def __init__(self, header_struct, name, code, size, width, number, location, area, + origin_station, vaac, wmo_id, header_number, elevation, year, advisory_number, + correction, info_source, additional_source, aviation_color, details, obs_date, + obs_time, obs_ash, forecast_6hr, forecast_12hr, forecast_18hr, remarks, + next_advisory, forecaster, offset_x, offset_y, lat, lon): + """Create volcano element. + + Parameters + ---------- + header_struct : `NamedStruct` + + name : str + + code : float + + size : float + + width : int + + number : str + + location : str + + area : str + + origin_station : str + + vaac : str + + wmo_id : str + + elevation : str + + year : int + + advisory_number : str + + correction : str + + info_source : str + + additional_source : str + + aviation_color : str + + details : str + + obs_date : str + + obs_time : str + + obs_ash : str + + forecat_6hr : str + + forecat_12hr : str + + forecat_18hr : str + + remarks : str + + next_advisory : str + + forecaster : str + + offset_x : int + + offset_y : int + + lat : `numpy.ndarray` + + lon : `numpy.ndarray` + """ + super().__init__(header_struct) + self.name = name + self.code = code + self.size = size + self.width = width + self.number = number + self.location = location + self.area = area + self.origin_station = origin_station + self.vaac = vaac + self.wmo_id = wmo_id + self.header_number = header_number + self.elevation = elevation + self.year = year + self.advisory_number = advisory_number + self.correction = correction + self.info_source = info_source + self.additional_source = additional_source + self.aviation_color = aviation_color + self.details = details + self.obs_date = obs_date + self.obs_time = obs_time + self.obs_ash = obs_ash + self.forecast_6hr = forecast_6hr + self.forecast_12hr = forecast_12hr + self.forecast_18hr = forecast_18hr + self.remarks = remarks + self.next_advisory = next_advisory + self.forecaster = forecaster + self.lat = lat + self.lon = lon + self.offset_x = offset_x + self.offset_y = offset_y + + +class WatchStatusMessageElement(SpecialLineElement): + """Watch status message element.""" + + def __init__(self, header_struct, number_points, line_type, stroke, direction, size, width, + lat, lon): + """Create watch status message line element. + + Parameters + ---------- + header_struct : `NamedStruct` + + number_points : int + + line_type : int + Integer code defining special line type. See Appendix C + in GEMPAK documentation for details. + + stroke : int + GEMPAK color code. + + direction : int + CW (1) or CCW (-1). + + size : float + Size of special line elements (e.g., dots, crosses, arrow heads, etc.). + + width : int + + lat : `numpy.ndarray` + + lon : `numpy.ndarray` + """ + super().__init__(header_struct, number_points, line_type, stroke, direction, size, + width, lat, lon) + + +class WindElement(VectorGraphicElement): + """Wind element.""" + + def __init__(self, header_struct, number_wind, width, size, wind_type, + head_size, speed, direction, lat, lon): + """Create wind element. + + Parameters + ---------- + header_struct : `NamedStruct` + + number_wind : int + Number of wind vectors. + + width : int + Vector line width. + + size : float + Scaling factor for wind vector. + + wind_type : int + GEMPAK integer code for wind. See WIND parameter + in GEMPAK GDPLOT documentation. + + head_size : float + Size of wind element head (barb or arrow). + + speed : float + Wind speed in knots. + + direction : float + Wind direction in degrees. + + lat : `numpy.ndarray` + + lon : `numpy.ndarray` + """ + super().__init__(header_struct) + self.number_wind = number_wind + self.width = width + self.size = size + self.wind_type = wind_type + self.head_size = head_size + self.speed = speed + self.direction = direction + self.lat = lat + self.lon = lon + + +class WatchBoxElement(VectorGraphicElement): + """Watch box element.""" + + def __init__(self, header_struct, number_points, style, shape, + marker_type, marker_size, marker_width, anchor0_station, + anchor0_lat, anchor0_lon, anchor0_distance, anchor0_direction, + anchor1_station, anchor1_lat, anchor1_lon, anchor1_distance, + anchor1_direction, status, number, issue_time, expire_time, + watch_type, severity, timezone, max_hail, max_wind, max_tops, + mean_storm_direction, mean_storm_speed, states, adjacent_areas, + replacing, forecaster, filename, issue_flag, wsm_issue_time, + wsm_expire_time, wsm_reference_direction, wsm_recent_from_line, + wsm_md_number, wsm_forecaster, number_counties, plot_counties, + county_fips, county_lat, county_lon, lat, lon): + """Create watch box element. + + Parameters + ---------- + header_struct : `NamedStruct` + + number_points : int + + style : int + Integer code designating watch-by-county (6) or parallelogram (4). + + shape : int + PGRAM watch shape. North-South (1), East-West (2), or either side + of line (3). + + marker_type : int + Integer code defining the county marker symbols. See Appendix C in + GEMPAK documentation for details. + + marker_size : float + + marker_width : int + + anchor0_station : str + Station ID of anchor point 0. + + anchor0_lat : float + Latitude of anchor point 0. + + anchor0_lon : float + Longitude of anchor point 0. + + anchor0_distance : int + Distance (statute miles) from anchor point 0. + + anchor0_direction : str + Compass direction (16-point) from anchor point 0. + + anchor1_station : str + Station ID of anchor point 1. + + anchor1_lat : float + Latitude of anchor point 1. + + anchor1_lon : float + Longitude of anchor point 1. + + anchor1_distance : int + Distance (statute miles) from anchor point 1. + + anchor1_direction : str + Compass direction (16-point) from anchor point 1. + + status : int + Active (1) or test (0). + + number : int + Watch number. + + issue_time : str + Issue time in format `%m/%d/%Y/%H%M/`. + + expire_time : str + Expiration time in format `%m/%d/%Y/%H%M/`. + + watch_type : int + Severe Thunderstorm (6) or Tornado (2). + + severity : int + PDS (1) or normal (0). + + timezone : str + Primary timezone. + + max_hail : str + Max hail size in inches. + + max_wind : str + Max wind gust in knots. + + max_tops : str + Max storm tops in hundreds of feet (e.g., 500 for 50000 ft.). + + mean_storm_direction : str + Mean storm direction in degrees. + + mean_storm_speed : str + Mean storm speed in knots. + + states : str + States in watch. + + adjacent_areas : str + Adjacent areas in watch. + + replacing : str + Watche numbers of watches being replaced. + + forecaster : str + Issuing forecaster name(s). + + filename : str + Wactch filename. + + issue_flag : int + Watch issued (1) or not issued (0). + + wsm_issue_time : str + Watch status message issue time in format `%d%H%M`. + + wsm_expire_time : str + Watch status message expiration time in format `%d%H%M`. + + wsm_reference direction : str + + wsm_recent_from_line : str + + wsm_md_number : str + Associated mesoscale discussion number. + + wsm_forecaster : str + Watch status message issuing forecaster name(s). + + number_counties : int + + plot_counties : int + Toggle for plotting couties. + + county_fips : `numpy.ndarray` + County FIPS codes. + + county_lat : `numpy.ndarray` + Latitude of county centroid. + + county_lon : `numpy.ndarray` + Longitude of county centroid. + + lat : `numpy.ndarray` + Latitude of parallelogram vertices. + + lon : `numpy.ndarray` + Longitude of parallelogram vertices. + """ + super().__init__(header_struct) + self.number_points = number_points + self.style = style + self.shape = shape + self.marker_type = marker_type + self.marker_size = marker_size + self.marker_width = marker_width + self.anchor0_station = anchor0_station + self.anchor0_lat = anchor0_lat + self.anchor0_lon = anchor0_lon + self.anchor0_distance = anchor0_distance + self.anchor0_direction = anchor0_direction + self.anchor1_station = anchor1_station + self.anchor1_lat = anchor1_lat + self.anchor1_lon = anchor1_lon + self.anchor1_distance = anchor1_distance + self.anchor1_direction = anchor1_direction + self.status = status + self.number = number + self.issue_time = issue_time + self.expire_time = expire_time + self.watch_type = watch_type + self.severity = severity + self.timezone = timezone + self.max_hail = max_hail + self.max_wind = max_wind + self.max_tops = max_tops + self.mean_storm_speed = mean_storm_speed + self.mean_storm_direction = mean_storm_direction + self.states = states + self.adjacent_areas = adjacent_areas + self.replacing = replacing + self.forecaster = forecaster + self.filename = filename + self.issue_flag = issue_flag + self.wsm_issue_time = wsm_issue_time + self.wsm_expire_time = wsm_expire_time + self.wsm_reference_direction = wsm_reference_direction + self.wsm_recent_from_line = wsm_recent_from_line + self.wsm_md_number = wsm_md_number + self.wsm_forecaster = wsm_forecaster + self.number_counties = number_counties + self.plot_counties = plot_counties + self.county_fips = county_fips + self.county_lat = county_lat + self.county_lon = county_lon + self.lat = lat + self.lon = lon + + self._set_properties() + + def _set_properties(self): + """Set properties using header and data.""" + self.marker_name = MarkerType(self.marker_type).name + + +class Group: + """GEMPAK VGF group.""" + + def __init__(self): + self._number_elements = 0 + self._elements = [] + self._group_type = None + self._group_number = None + + def __iter__(self): + """Return iterator of group elements.""" + yield from self._elements + + def __repr__(self): + """Return repr(self).""" + return (f'{type(self).__qualname__}' + f'[{self.group_type}, {self.group_number}]') + + @property + def elements(self): + """Get elements.""" + return self._elements + + @property + def group_size(self): + """Get numerber of elements in group.""" + return self._number_elements + + @property + def group_type(self): + """Get group type.""" + return self._group_type + + @property + def group_number(self): + """Get group number.""" + return self._group_number + + def add_element(self, element): + """Add element to group. + + Parameters + ---------- + element : subclass of `mdgpu.io.vgf.VectorGraphicElement` + """ + if (self._group_type != element.group_type + and self._group_type is None): + self._group_type = element.group_type + elif self._group_type == element.group_type: + pass + else: + raise ValueError('Group cannot have multiple types.') + + if (self._group_number != element.group_number + and self._group_number is None): + self._group_number = element.group_number + elif self._group_number == element.group_number: + pass + else: + raise ValueError('Cannot have multiple groups.') + + self._elements.append(element) + self._number_elements += 1 + + +class NumpyEncoder(json.JSONEncoder): + """JSONEncoder class extension for numpy arrays.""" + + def default(self, obj): + """`NumpyEncoder` default serializer.""" + if isinstance(obj, np.ndarray): + return [float(f'{x:6.2f}') for x in obj] + return json.JSONEncoder.default(self, obj) + + +class VectorGraphicFile: + """GEMPAK Vector Graphics Format decoder class.""" + + def __init__(self, file): + """Read and decode a GEMPAK Vector Graphic file. + + Parameters + ---------- + file : str or `pathlib.Path` + """ + with contextlib.closing(open(file, 'rb')) as fobj: # noqa: SIM115 + self._buffer = IOBuffer.fromfile(fobj) + + if sys.byteorder == 'little': + self.prefmt = '>' + else: + self.prefmt = '' + + self._elements = [] + self._groups = [] + + self._decode_elements() + + @property + def groups(self): + """Get present group types.""" + return self._groups + + @property + def elements(self): + """Get elements.""" + return self._elements + + @property + def header(self): + """Get file header.""" + return self._header + + @property + def bounds(self): + """Get bounding box of all elements.""" + xmin = 9999 + xmax = -9999 + ymin = 9999 + ymax = -9999 + + for element in self.elements: + if 0 in [element.max_lat, element.max_lon, + element.min_lat, element.min_lon]: + continue + else: + if element.max_lat > ymax: + ymax = element.max_lat + if element.max_lon > xmax: + xmax = element.max_lon + if element.min_lat < ymin: + ymin = element.min_lat + if element.min_lon < xmin: + xmin = element.min_lon + + return ymin, xmin, ymax, xmax + + @property + def has_fronts(self): + """Check for front elements.""" + return bool(self.filter_elements(vg_class=VGClass.fronts.value)) + + @property + def has_text(self): + """Check for text elements.""" + return bool(self.filter_elements(vg_class=VGClass.text.value, + vg_type=VGType.text.value)) + + @property + def has_special_text(self): + """Check for special text elements.""" + return bool(self.filter_elements(vg_class=VGClass.text.value, + vg_type=VGType.special_text.value)) + + @property + def has_symbols(self): + """Check for symbols elements.""" + return bool(self.filter_elements(vg_class=VGClass.symbols.value)) + + @property + def has_special_lines(self): + """Check for special lines elements.""" + return bool(self.filter_elements(vg_class=VGClass.lines.value, + vg_type=VGType.special_line.value)) + + @property + def has_lines(self): + """Check for lines elements.""" + return bool(self.filter_elements(vg_class=VGClass.lines.value, + vg_type=VGType.line.value)) + + @property + def has_winds(self): + """Check for wind elements.""" + return bool(self.filter_elements(vg_class=VGClass.winds.value)) + + @property + def has_tracks(self): + """Check for track elements.""" + return bool(self.filter_elements(vg_class=VGClass.tracks.value)) + + @property + def has_watch_box(self): + """Check for watch box elements.""" + return bool(self.filter_elements(vg_class=VGClass.watches.value)) + + @property + def has_sigmet(self): + """Check for SIGMET elements.""" + return bool(self.filter_elements(vg_class=VGClass.sigmets.value)) + + @property + def has_met(self): + """Check for MET elements.""" + return bool(self.filter_elements(vg_class=VGClass.met.value)) + + def get_fronts(self): + """Extract front elements. + + Returns + ------- + List of `mdgpu.io.vgf.FrontElement`. + """ + if self.has_fronts: + return self.filter_elements(vg_class=VGClass.fronts.value) + + return None + + def get_text(self): + """Extract text elements. + + Returns + ------- + List of `mdgpu.io.vgf.TextElement`. + """ + if self.has_text: + return self.filter_elements(vg_class=VGClass.text.value) + + return None + + def get_special_text(self): + """Extract special text elements. + + Returns + ------- + List of `mdgpu.io.vgf.SpecialTextElement`. + """ + if self.has_special_text: + return self.filter_elements(vg_class=VGClass.text.value, + vg_type=VGType.special_text.value) + + return None + + def get_symbols(self): + """Extract symbol elements. + + Returns + ------- + List of `mdgpu.io.vgf.SymbolElement`. + """ + if self.has_symbols: + return self.filter_elements(vg_class=VGClass.symbols.value) + + return None + + def get_special_lines(self): + """Extract special line elements. + + Returns + ------- + List of `mdgpu.io.vgf.SpecialLineElement`. + + Notes + ----- + This will exclude the MD area line. + """ + if self.has_special_lines: + special_lines = self.filter_elements(vg_class=VGClass.lines.value, + vg_type=VGType.special_line.value) + return [x for x in special_lines + if x.line_type != SpecialLineType.scallop.value] + + return None + + def get_lines(self): + """Extract line elements. + + Returns + ------- + List of `mdgpu.io.vgf.LineElement`. + """ + if self.has_lines: + return self.filter_elements(vg_class=VGClass.lines.value, + vg_type=VGType.line.value) + + return None + + def get_tracks(self): + """Extract track elements. + + Returns + ------- + List of `mdgpu.io.vgf.TrackElement`. + """ + if self.has_tracks: + return self.filter_elements(vg_class=VGClass.tracks.value) + + return None + + def get_winds(self): + """Extract wind elements. + + Returns + ------- + List of `mdgpu.io.vgf.WindElement`. + """ + if self.has_winds: + return self.filter_elements(vg_class=VGClass.winds.value) + + return None + + def get_watch_box(self): + """Extract watch box elements. + + Returns + ------- + List of `mdgpu.io.vgf.WatchBoxElement`. + """ + if self.has_watch_box: + return self.filter_elements(vg_class=VGClass.watches.value) + + return None + + def get_sigmet(self): + """Extract SIGMET elements. + + Returns + ------- + List of `mdgpu.io.vgf.SigmetElement`. + """ + if self.has_sigmet: + return self.filter_elements(vg_class=VGClass.sigmets.value) + + return None + + def get_met(self): + """Extract MET elements. + + Returns + ------- + List of MET elements. + """ + if self.has_sigmet: + return self.filter_elements(vg_class=VGClass.met.value) + + return None + + def get_group(self, group_type): + """Get elements that are part of a group type. + + Parameters + ---------- + group_type : int + + Returns + ------- + List of `mdgpu.io.vgf.VectorGraphicElement` subclasses in a group. + """ + type_temp = [] + numbers = [] + grouped = [] + + for element in self._elements: + if element.group_type == group_type: + type_temp.append(element) + + for element in type_temp: + if element.group_number not in numbers: + numbers.append(element.group_number) + + for gn in sorted(numbers): + g = Group() + for element in type_temp: + if element.group_number == gn: + g.add_element(element) + if g.group_size: + grouped.append(g) + + if not grouped: + grouped = None + + return grouped + + def filter_elements(self, vg_class=None, vg_type=None, operation='and'): + """Filter elements by class and type. + + Parameters + ---------- + vg_class : int + Integer code for vector graphic classes. + + vg_type : int + Integer code for vector graphic types. + + operation : str + How queries should be handled when vg_class and vg_type are both + used. `and` is logical and and `or` uses logical or. The default + is `and`. + + Returns + ------- + List of `mdgpu.io.vgf.VectorGraphicElement` subclasses. + """ + if operation not in ['and', 'or']: + raise ValueError(f'Illegal operation `{operation}`.') + + filtered = [] + if vg_class is not None and vg_type is None: + for e in self.elements: + if e.vg_class == vg_class: + filtered.append(e) + elif vg_class is None and vg_type is not None: + for e in self.elements: + if e.vg_type == vg_type: + filtered.append(e) + elif vg_class is not None and vg_type is not None: + if operation == 'and': + for e in self.elements: + if e.vg_class == vg_class and e.vg_type == vg_type: + filtered.append(e) + elif operation == 'or': + for e in self.elements: + if e.vg_class == vg_class or e.vg_type == vg_type: + filtered.append(e) + + if not filtered: + filtered = None + + return filtered + + def to_json(self, **kwargs): + """Convert VGF elements to JSON. + + Parameters + ---------- + kwargs : Keyword arguments to pass to `json.dumps`. + + Returns + ------- + string + JSON string representation of VGF elements. + """ + serialized = [e.__dict__ for e in self._elements] + + return json.dumps(serialized, + cls=kwargs.get('cls', NumpyEncoder), + **kwargs) + + def _decode_elements(self): + """Decode elements of a VGF.""" + while not self._buffer.at_end(): + header_struct = self._read_header() + rec_size = header_struct.record_size + vg_type = header_struct.vg_type + vg_class = header_struct.vg_class + data_size = rec_size - VGF_HEADER_SIZE + + group_info = header_struct.group_type + + # Ignores the file header group + if (group_info not in self._groups + and group_info + and vg_class != VGClass.header.value): + self._groups.append(group_info) + + if vg_class == VGClass.header.value and vg_type == VGType.file_header.value: + version_size = 128 + version = self._decode_strip_null(self._buffer.read(version_size)) + notes_size = data_size - version_size + notes = self._decode_strip_null(self._buffer.read(notes_size)) + self._header = FileHeaderElement(header_struct, version, notes) + elif vg_class == VGClass.fronts.value: + front_info = [ + ('number_points', 'i'), ('front_code', 'i'), ('pip_size', 'i'), + ('pip_stroke', 'i'), ('pip_direction', 'i'), ('width', 'i'), + ('label', '4s', self._decode_strip_null) + ] + front = self._buffer.read_struct( + NamedStruct(front_info, self.prefmt, 'FrontInfo') + ) + + lat, lon = self._get_latlon(front.number_points) + + self._elements.append( + FrontElement(header_struct, front.number_points, front.front_code, + front.pip_size, front.pip_stroke, front.pip_direction, + front.width, front.label, lat, lon) + ) + elif vg_class == VGClass.symbols.value: + symbol_info = [ + ('number_symbols', 'i'), ('width', 'i'), + ('symbol_size', 'f', self._round_one), ('symbol_type', 'i'), + ('symbol_code', 'f', int), ('lat', 'f', self._round_two), + ('lon', 'f', self._round_two), ('offset_x', 'i'), ('offset_y', 'i') + ] + symbol = self._buffer.read_struct( + NamedStruct(symbol_info, self.prefmt, 'SymbolInfo') + ) + + self._elements.append( + SymbolElement(header_struct, symbol.number_symbols, symbol.width, + symbol.symbol_size, symbol.symbol_type, symbol.symbol_code, + symbol.offset_x, symbol.offset_y, symbol.lat, symbol.lon) + ) + elif vg_class == VGClass.circle.value: + if vg_type == VGType.circle.value: + line_info = [ + ('number_points', 'i'), ('line_type', 'i'), + ('line_type_hardware', 'i'), ('width', 'i'), + ('line_width_hardware', 'i') + ] + line = self._buffer.read_struct( + NamedStruct(line_info, self.prefmt, 'LineInfo') + ) + + # This is CircData + lat, lon = self._get_latlon(line.number_points) + + self._elements.append( + CircleElement(header_struct, line.number_points, line.line_type, + line.line_type_hardware, line.width, + line.line_width_hardware, lat, lon) + ) + elif vg_class == VGClass.lines.value: + if vg_type == VGType.line.value: + line_info = [ + ('number_points', 'i'), ('line_type', 'i'), + ('line_type_hardware', 'i'), ('width', 'i'), + ('line_width_hardware', 'i') + ] + line = self._buffer.read_struct( + NamedStruct(line_info, self.prefmt, 'LineInfo') + ) + + lat, lon = self._get_latlon(line.number_points) + if isinstance(lat, int) and lat == -9999: + continue + + if header_struct.closed and line.number_points > 2: + lon, lat = self.close_coordinates(lon, lat) + + self._elements.append( + LineElement(header_struct, line.number_points, line.line_type, + line.line_type_hardware, line.width, + line.line_width_hardware, lat, lon) + ) + elif vg_type == VGType.special_line.value: + special_line_info = [ + ('number_points', 'i'), ('line_type', 'i'), ('stroke', 'i'), + ('direction', 'i'), ('line_size', 'f', self._round_one), ('width', 'i') + ] + special_line = self._buffer.read_struct( + NamedStruct(special_line_info, self.prefmt, 'SpecialLineInfo') + ) + + lat, lon = self._get_latlon(special_line.number_points) + if isinstance(lat, int) and lat == -9999: + continue + + if header_struct.closed and special_line.number_points > 2: + lon, lat = self.close_coordinates(lon, lat) + + if special_line.direction == -1: + lon, lat = self.flip_coordinates(lon, lat) + + self._elements.append( + SpecialLineElement(header_struct, special_line.number_points, + special_line.line_type, special_line.stroke, + special_line.direction, special_line.line_size, + special_line.width, lat, lon) + ) + else: + raise NotImplementedError(f'Line type `{vg_type}` is not implemented.') + elif vg_class == VGClass.lists.value: + list_info = [ + ('list_type', 'i'), ('marker_type', 'i'), + ('marker_size', 'f', self._round_one), + ('marker_width', 'i'), ('number_items', 'i') + ] + list_struct = NamedStruct(list_info, self.prefmt, 'ListInfo') + list_meta = self._buffer.read_struct(list_struct) + + list_items = [ + self._buffer.read_ascii(LIST_MEMBER_SIZE).replace('\x00', '') + for _n in range(list_meta.number_items) + ] + list_item_blank_size = (MAX_POINTS - list_meta.number_items) * LIST_MEMBER_SIZE + self._buffer.skip(list_item_blank_size) + + coord_blank_size = 4 * (MAX_POINTS - list_meta.number_items) + lat = self._buffer.read_array(list_meta.number_items, f'{self.prefmt}f') + self._buffer.skip(coord_blank_size) + lon = self._buffer.read_array(list_meta.number_items, f'{self.prefmt}f') + self._buffer.skip(coord_blank_size) + + self._elements.append( + ListElement(header_struct, list_meta.list_type, list_meta.marker_type, + list_meta.marker_size, list_meta.marker_width, + list_meta.number_items, list_items, lat, lon) + ) + elif vg_class == VGClass.text.value: + if vg_type == VGType.text.value or vg_type == VGType.justified_text.value: + text_info = [ + ('rotation', 'f', self._round_one), + ('text_size', 'f', self._round_one), ('font', 'i'), + ('text_flag', 'i'), ('width', 'i'), ('align', 'i'), + ('lat', 'f', self._round_two), ('lon', 'f', self._round_two), + ('offset_x', 'i'), ('offset_y', 'i') + ] + text_struct = NamedStruct(text_info, self.prefmt, 'TextInfo') + text = self._buffer.read_struct(text_struct) + + text_length = rec_size - VGF_HEADER_SIZE - text_struct.size + text_string = self._buffer.read_ascii(text_length) + clean_text = text_string.replace('$$', '\n').replace('\x00', '').strip() + + self._elements.append( + TextElement(header_struct, text.rotation, text.text_size, text.font, + text.text_flag, text.width, text.align, text.lat, + text.lon, text.offset_x, text.offset_y, clean_text) + ) + elif vg_type == VGType.special_text.value: + special_text_info = [ + ('rotation', 'f', self._round_one), + ('text_size', 'f', self._round_one), ('text_type', 'i'), + ('turbulence_symbol', 'i'), ('font', 'i'), ('text_flag', 'i'), + ('width', 'i'), ('text_color', 'i'), ('line_color', 'i'), + ('fill_color', 'i'), ('align', 'i'), ('lat', 'f', self._round_two), + ('lon', 'f', self._round_two), ('offset_x', 'i'), ('offset_y', 'i') + ] + text_struct = NamedStruct( + special_text_info, self.prefmt, 'SpecialTextInfo' + ) + text = self._buffer.read_struct(text_struct) + + text_length = rec_size - VGF_HEADER_SIZE - text_struct.size + text_string = self._buffer.read_ascii(text_length) + clean_text = text_string.replace('$$', '\n').replace('\x00', '').strip() + + self._elements.append( + SpecialTextElement(header_struct, text.rotation, text.text_size, + text.text_type, text.turbulence_symbol, text.font, + text.text_flag, text.width, text.text_color, + text.line_color, text.fill_color, text.align, + text.lat, text.lon, text.offset_x, text.offset_y, + clean_text) + ) + else: + raise NotImplementedError(f'Text type `{vg_type}` is not implemented.') + elif vg_class == VGClass.tracks.value: + track_info = [ + ('track_type', 'i'), ('total_points', 'i'), ('initial_points', 'i'), + ('initial_line_type', 'i'), ('extrapolated_line_type', 'i'), + ('initial_mark_type', 'i'), ('extrapolated_mark_type', 'i'), + ('line_width', 'i'), ('speed', 'f', self._round_two), + ('direction', 'f', self._round_two), ('increment', 'i'), + ('skip', 'i'), ('font', 'i'), ('font_flag', 'i') + ] + track = self._buffer.read_struct( + NamedStruct(track_info, self.prefmt, 'TrackInfo') + ) + + # sztext seems to always be little endian. It is not swapped like other + # elements in the storm track struct. See storm track section of cvgswap.c. + track_font_size = round(self._buffer.read_binary(1, ')(.+?)(?=<)', tca_string + ).group()) + + issue_status = re.search( + r'(?<=)(.+?)(?=<)', tca_string + ).group() + + basin = int(re.search( + r'(?<=)(.+?)(?=<)', tca_string + ).group()) + + advisory_number = int(re.search( + r'(?<=)(.+?)(?=<)', tca_string + ).group()) + + storm_name = re.search( + r'(?<=)(.+?)(?=<)', tca_string + ).group() + + storm_type = int(re.search( + r'(?<=)(.+?)(?=<)', tca_string + ).group()) + + valid = re.search( + r'(?<=)(.+?)(?=<)', tca_string + ).group() + + tz = re.search( + r'(?<=)(.+?)(?=<)', tca_string + ).group() + + text_lat = float(re.search( + r'(?<=)(.+?)(?=<)', tca_string + ).group()) + + text_lon = float(re.search( + r'(?<=)(.+?)(?=<)', tca_string + ).group()) + + text_font = int(re.search( + r'(?<=)(.+?)(?=<)', tca_string + ).group()) + + text_size = float(re.search( + r'(?<=)(.+?)(?=<)', tca_string + ).group()) + + text_width = int(re.search( + r'(?<=)(.+?)(?=<)', tca_string + ).group()) + + wwnum = int(re.search( + r'(?<=)(.+?)(?=<)', tca_string + ).group()) + + ww = [] + for n in range(wwnum): + wwstr = re.search( + rf'(?<=)(.+?)(?=<)', tca_string + ).group() + nbreaks = int(re.search( + rf'(?<=)(.+?)(?=<)', tca_string + ).group()) + breakpts = re.search( + rf'(?<=)(.+?)(?=<|$)', tca_string + ).group() + + severity, advisory_type, special_geog = wwstr.split('|') + + parsed_breaks = np.array_split(breakpts.split('|'), nbreaks) + decode_breaks = [[float(lat), float(lon), bname] + for lat, lon, bname in parsed_breaks] + + ww.append( + { + 'severity': Severity(int(severity)), + 'advisory_type': AdvisoryType(int(advisory_type)), + 'special_geography': SpecialGeography(int(special_geog)), + 'number_breaks': nbreaks, + 'break_points': decode_breaks + } + ) + + self._elements.append( + TropicalCycloneAdvisoryElement( + header_struct, storm_number, issue_status, basin, + advisory_number, storm_name, storm_type, valid, tz, text_lat, + text_lon, text_font, text_size, text_width, wwnum, ww) + ) + elif vg_type in [VGType.tc_error_cone.value, VGType.tc_track.value, + VGType.tc_break_point.value]: + tc_info = [ + ('storm_number', '5s', self._decode_strip_null), + ('issue_status', '2s', self._decode_strip_null), + ('basin', '5s', self._decode_strip_null), + ('advisory_number', '5s', self._decode_strip_null), + ('storm_name', '128s', self._decode_strip_null), + ('storm_type', '5s', self._decode_strip_null), + ('valid_time', '21s', self._decode_strip_null), + ('timezone', '4s', self._decode_strip_null), + ('forecast_period', '5s', self._decode_strip_null) + ] + + tc = self._buffer.read_struct( + NamedStruct(tc_info, self.prefmt, 'TCInfo') + ) + + if vg_type == VGType.tc_error_cone.value: + cone_info = [ + ('line_color', 'i'), ('line_type', 'i'), ('fill_color', 'i'), + ('fill_type', 'i'), ('number_points', 'i') + ] + + cone = self._buffer.read_struct( + NamedStruct(cone_info, self.prefmt, 'TCConeInfo') + ) + + lat, lon = self._get_latlon(cone.number_points) + + self._elements.append( + TropicalCycloneErrorElement( + header_struct, tc.storm_number, tc.issue_status, tc.basin, + tc.advisory_number, tc.storm_name, tc.storm_type, + tc.valid_time, tc.timezone, tc.forecast_period, + cone.line_color, cone.line_type, cone.fill_color, + cone.fill_type, cone.number_points, lat, lon) + ) + elif vg_type == VGType.tc_track.value: + track_info = [ + ('line_color', 'i'), ('line_type', 'i'), ('number_points', 'i') + ] + + track = self._buffer.read_struct( + NamedStruct(track_info, self.prefmt, 'TCTrackInfo') + ) + + track_point_info = [ + ('lat', 'f', self._round_two), ('lon', 'f', self._round_two), + ('advisory_date', '50s', self._decode_strip_null), + ('tau', '50s', self._decode_strip_null), + ('max_wind', '50s', self._decode_strip_null), + ('wind_gust', '50s', self._decode_strip_null), + ('minimum_pressure', '50s', self._decode_strip_null), + ('development_level', '50s', self._decode_strip_null), + ('development_label', '50s', self._decode_strip_null), + ('direction', '50s', self._decode_strip_null), + ('speed', '50s', self._decode_strip_null), + ('date_label', '50s', self._decode_strip_null), + ('storm_source', '50s', self._decode_strip_null), + (None, '2x') # skip struct alignment padding bytes + ] + + point_struct = NamedStruct(track_point_info, self.prefmt, + 'TrackPointInfo') + + track_points = [] + for _ in range(track.number_points): + pt = self._buffer.read_struct(point_struct) + track_points.append( + TrackAttribute( + pt.advisory_date, pt.tau, pt.max_wind, pt.wind_gust, + pt.minimum_pressure, pt.development_level, + pt.development_label, pt.direction, pt.speed, + pt.date_label, pt.storm_source, pt.lat, pt.lon) + ) + + self.elements.append( + TropicalCycloneTrackElement( + header_struct, tc.storm_number, tc.issue_status, tc.basin, + tc.advisory_number, tc.storm_name, tc.storm_type, + tc.valid_time, tc.timezone, tc.forecast_period, + track.line_color, track.line_type, track.number_points, + track_points) + ) + elif vg_type == VGType.tc_break_point.value: + break_info = [ + ('line_color', 'i'), ('line_width', 'i'), ('ww_level', 'i'), + ('number_points', 'i') + ] + + brkpt = self._buffer(break_info, self.prefmt, + 'BreakPointInfo') + + break_meta = [ + ('lat', 'f', self._round_two), ('lon', 'f', self._round_two), + ('name', '256s', self._decode_strip_null) + ] + + break_struct = NamedStruct(break_meta, self.prefmt, 'BreakPoint') + + breakpoints = [] + for _ in range(brkpt.number_points): + bp = self._buffer.read_struct(break_struct) + breakpoints.append( + BreakPointAttribute(bp.lat, bp.lon, bp.name) + ) + + self._elements.append( + TropicalCycloneBreakPointElement( + header_struct, tc.storm_number, tc.issue_status, + tc.basin, tc.advisory_number, tc.storm_name, + tc.storm_type, tc.valid_time, tc.timezone, + tz.forecast_period, brkpt.line_color, brkpt.line_width, + brkpt.ww_level, brkpt.number_points, breakpoints + ) + ) + elif vg_type == VGType.sgwx.value: + sgwx_info = [ + ('subtype', 'i'), ('number_points', 'i'), + ('text_lat', 'f', self._round_two), + ('text_lon', 'f', self._round_two), + ('arrow_lat', 'f', self._round_two), + ('arrow_lon', 'f', self._round_two), + ('line_element', 'i'), ('line_type', 'i'), + ('line_width', 'i'), ('arrow_size', 'f', self._round_one), + ('special_symbol', 'i'), ('weather_symbol', 'i') + ] + + sgwx = self._buffer.read_struct( + NamedStruct(sgwx_info, self.prefmt, 'SGWXInfo') + ) + + special_text_info = [ + ('rotation', 'f', self._round_one), + ('text_size', 'f', self._round_one), ('text_type', 'i'), + ('turbulence_symbol', 'i'), ('font', 'i'), ('text_flag', 'i'), + ('width', 'i'), ('text_color', 'i'), ('line_color', 'i'), + ('fill_color', 'i'), ('align', 'i'), + ('lat', 'f', self._round_two), ('lon', 'f', self._round_two), + ('offset_x', 'i'), ('offset_y', 'i') + ] + + text = self._buffer.read_struct( + NamedStruct(special_text_info, self.prefmt, 'SpecialTextInfo') + ) + + lat, lon = self._get_latlon(sgwx.number_points) + + self._elements.append( + SignificantWeatherElement( + header_struct, sgwx.subtype, sgwx.number_points, + sgwx.text_lat, sgwx.text_lon, sgwx.arrow_lat, + sgwx.arrow_lon, sgwx.line_element, sgwx.line_type, + sgwx.line_width, sgwx.arrow_size, sgwx.special_symbol, + sgwx.weather_symbol, text.rotation, text.text_size, + text.text_type, text.turbulence_symbol, text.font, + text.text_flag, text.text_width, text.text_color, + text.line_color, text.fill_color, text.text_align, + text.offset_x, text.offset_y, text.text, lat, lon + ) + ) + + self. _buffer.skip((MAX_SGWX_POINTS - sgwx.number_points) * 4) + else: + raise NotImplementedError(f'MET type `{vg_type}` not implemented.') + elif vg_class == VGClass.watches.value: + watch_info = [ + ('number_points', 'i'), ('style', 'i'), ('shape', 'i'), + ('marker_type', 'i'), ('marker_size', 'f', self._round_one), + ('marker_width', 'i'), ('anchor0_station', '8s', self._decode_strip_null), + ('anchor0_lat', 'f', self._round_two), + ('anchor0_lon', 'f', self._round_two), ('anchor0_distance', 'i'), + ('anchor0_direction', '4s', self._decode_strip_null), + ('anchor1_station', '8s', self._decode_strip_null), + ('anchor1_lat', 'f', self._round_two), + ('anchor1_lon', 'f', self._round_two), ('anchor1_distance', 'i'), + ('anchor1_direction', '4s', self._decode_strip_null), ('status', 'i'), + ('number', 'i'), ('issue_time', '20s', self._decode_strip_null), + ('expire_time', '20s', self._decode_strip_null), ('watch_type', 'i'), + ('severity', 'i'), ('timezone', '4s', self._decode_strip_null), + ('max_hail', '8s', self._decode_strip_null), + ('max_wind', '8s', self._decode_strip_null), + ('max_tops', '8s', self._decode_strip_null), + ('mean_storm_direction', '8s', self._decode_strip_null), + ('mean_storm_speed', '8s', self._decode_strip_null), + ('states', '80s', self._decode_strip_null), + ('adjacent_areas', '80s', self._decode_strip_null), + ('replacing', '24s', self._decode_strip_null), + ('forecaster', '64s', self._decode_strip_null), + ('filename', '128s', self._decode_strip_null), + ('issue_flag', 'i'), ('wsm_issue_time', '20s', self._decode_strip_null), + ('wsm_expire_time', '20s', self._decode_strip_null), + ('wsm_reference_direction', '32s', self._decode_strip_null), + ('wsm_recent_from_line', '128s', self._decode_strip_null), + ('wsm_md_number', '8s', self._decode_strip_null), + ('wsm_forecaster', '64s', self._decode_strip_null), + ('number_counties', 'i'), ('plot_counties', 'i') + ] + + watch = self._buffer.read_struct( + NamedStruct(watch_info, self.prefmt, 'WatchBoxInfo') + ) + + county_fips = self._buffer.read_array(watch.number_counties, f'{self.prefmt}i') + county_fips_blank_size = 4 * (MAX_COUNTIES - watch.number_counties) + self._buffer.skip(county_fips_blank_size) + + county_lat = self._buffer.read_array(watch.number_counties, f'{self.prefmt}f') + county_lon = self._buffer.read_array(watch.number_counties, f'{self.prefmt}f') + county_loc_blank_size = 8 * (MAX_COUNTIES - watch.number_counties) + self._buffer.skip(county_loc_blank_size) + + lat = self._buffer.read_array(watch.number_points, f'{self.prefmt}f') + lon = self._buffer.read_array(watch.number_points, f'{self.prefmt}f') + + # Manually close watch parallelogram + if watch.number_points > 2: + lon, lat = self.close_coordinates(lon, lat) + + self._elements.append( + WatchBoxElement(header_struct, watch.number_points, watch.style, + watch.shape, watch.marker_type, watch.marker_size, + watch.marker_width, watch.anchor0_station, + watch.anchor0_lat, watch.anchor0_lon, + watch.anchor0_distance, watch.anchor0_direction, + watch.anchor1_station, watch.anchor1_lat, + watch.anchor1_lon, watch.anchor1_distance, + watch.anchor1_direction, watch.status, + watch.number, watch.issue_time, + watch.expire_time, watch.watch_type, + watch.severity, watch.timezone, + watch.max_hail, watch.max_wind, + watch.max_tops, watch.mean_storm_direction, + watch.mean_storm_speed, watch.states, + watch.adjacent_areas, watch.replacing, + watch.forecaster, watch.filename, watch.issue_flag, + watch.wsm_issue_time, watch.wsm_expire_time, + watch.wsm_reference_direction, + watch.wsm_recent_from_line, watch.wsm_md_number, + watch.wsm_forecaster, watch.number_counties, + watch.plot_counties, county_fips, county_lat, + county_lon, lat, lon) + ) + elif vg_class == VGClass.winds.value: + wind_info = [ + ('number_wind', 'i'), ('width', 'i'), ('size', 'f', self._round_one), + ('wind_type', 'i'), ('head_size', 'f', self._round_one), + ('speed', 'f', self._round_two), ('direction ', 'f', self._round_two), + ('lat', 'f', self._round_two), ('lon', 'f', self._round_two) + ] + wind = self._buffer.read_struct( + NamedStruct(wind_info, self.prefmt, 'WindInfo') + ) + + self._elements.append( + WindElement(header_struct, wind.number_wind, wind.width, wind.size, + wind.wind_type, wind.head_size, wind.speed, wind.direction, + wind.lat, wind.lon) + ) + else: + logger.warning('Could not decode element with class `%s` and type `%s`', + VGClass(vg_class).name, VGType(vg_type).name) + _ = self._buffer.skip(data_size) + + def _get_latlon(self, points): + """Extract latitude and longitude from VGF element. + + Parameters + ---------- + points : int + Number of points to be decoded. + + Returns + ------- + tuple of lat, lon of `points` dimension + """ + truncated = points > MAX_POINTS + if truncated: + raise ValueError('Exceeded maximum number of points in element.') + else: + if points >= 1: + lat = np.around(self._buffer.read_array(points, f'{self.prefmt}f'), 2) + lon = np.around(self._buffer.read_array(points, f'{self.prefmt}f'), 2) + else: + lat = -9999 + lon = -9999 + + return lat, lon + + def _read_header(self): + """Read VGF header. + + Notes + ----- + Header size should be 40 bytes (see GEMPAK vgstruct.h). + """ + vgf_header_info = [ + ('delete', 'c', ord), ('vg_type', 'c', ord), ('vg_class', 'c', ord), + ('filled', 'b'), ('closed', 'c', ord), ('smooth', 'c', ord), + ('version', 'c', ord), ('group_type', 'c', ord), ('group_number', 'i'), + ('major_color', 'i'), ('minor_color', 'i'), ('record_size', 'i'), + ('min_lat', 'f', self._round_two), ('min_lon', 'f', self._round_two), + ('max_lat', 'f', self._round_two), ('max_lon', 'f', self._round_two) + ] + + return self._buffer.read_struct(NamedStruct( + vgf_header_info, self.prefmt, 'Header' + )) + + @staticmethod + def close_coordinates(x, y): + """Close polygon coordinates. + + Parameters + ---------- + x : array_like + + y : array_like + + Returns + ------- + tuple of x, y of closed polygon + """ + if isinstance(x, np.ndarray) and isinstance(y, np.ndarray): + x = np.concatenate([x, [x[0]]]) + y = np.concatenate([y, [y[0]]]) + elif isinstance(x, list) and isinstance(y, list): + x += x[0] + y += y[0] + else: + raise TypeError('x and y must be of the same type: list or array.') + + return x, y + + @staticmethod + def flip_coordinates(x, y): + """Flip coordinate direction. + + Parameters + ---------- + x : array_like + + y : array_like + + Returns + ------- + tuple of reversed x, y + """ + if ((isinstance(x, np.ndarray) and isinstance(y, np.ndarray)) + or (isinstance(x, list) and isinstance(y, list))): + x = x[::-1] + y = y[::-1] + else: + raise TypeError('x and y must be of the same type: list or array.') + + return x, y + + @staticmethod + def _decode_strip_null(x): + """Decode bytes into string and truncate based on null terminator. + + Parameters + ---------- + x : bytes + + Returns + ------- + str with whitespace and null stripped + + Notes + ----- + The bytes array is first decoded UTF-8 to get rid of non-UTF-8 + characters. GEMPAK used static char array sizes and often had + junk data after the string. The string is properly truncated + after finding the null terminator and then whitespace is + stripped. + """ + decoded = x.decode('utf-8', errors='ignore') + null = decoded.find('\x00') + return decoded[:null].strip() + + @staticmethod + def _round_one(x): + """Round to one decimal. + + Parameters + ---------- + x : float + + Returns + ------- + float rounded to 1 decimal + """ + return round(x, 1) + + @staticmethod + def _round_two(x): + """Round to two decimals. + + Parameters + ---------- + x : float + + Returns + ------- + float rounded to 2 decimals + """ + return round(x, 2) + + @staticmethod + def _swap32(x): + """Swap bytes of 32-bit float or int. + + Parameters + ---------- + x : float or int + + Returns + ------- + byte-swapped float or int + """ + return int.from_bytes(x.to_bytes(4, byteorder='little'), + byteorder='big', signed=False) diff --git a/tests/data/fills.vgf b/tests/data/fills.vgf new file mode 100644 index 0000000000000000000000000000000000000000..b5981eba7529937fa50560fd3de4e3f0fa06a284 GIT binary patch literal 1132 zcmZP&W55C!SD*`qr4|)u=I1Gx>lvEr8KBG3p6}-v?imoQ5SE%;l3%3YUX)mnk(sRE zmS2>cSfW6C>j&NjMrI~PCb%;|EM_21anj5ZJM>Oc+sSrT!J+%-1b||!K+FilAONPF zbUy5HGV9$5QsZP~t`5_4=*^wBLr?sSK>7|nb=PEIWM;-{2T$SaL$5sNJ1IuHAG-7G z1JGPdJA~GsbCRC_8l=WaWUh!4RL`N86*CS!STYNw@6h8OexMyJSnbf;Eq~~mm5r0* zir0sZnU`U;!}4nyNZ!dhVw018<}D|vom6Yal&`W^a!@wDjT@071dmrvLx| literal 0 HcmV?d00001 diff --git a/tests/data/fronts.vgf b/tests/data/fronts.vgf new file mode 100644 index 0000000000000000000000000000000000000000..885ae48957d6a5b1e01805b261f8438beb1a879f GIT binary patch literal 1944 zcmc(fTSydP7=~weJ>V%#)U?z_(@0Qp!BjGl`65r08J?mn5O+yoJ!a)a1QLQoR2L;n zQg-2m!Da`NthThzHgVh3(o`x>BwC?jhYP{NzJDBzmYGXw;A3|GectEY{n`0BKThUS zwb(D_>8nnZl$Y`G(b}ESG4`LX|NBfKKRqj(&(jxGmsjzrRfQGBB}IHnc~xm)HNXD# z|Gz=4;yCr%*KnX$YzjboaTGt-T?ZIHhrOZMw5H)WRWsdnG^%B~=&sJr$>10Z33^^+ zDT5yjC}x!0ab9NpGVvNxT7vM6AysnUl64aci9ZgBEM;)KzER95xg&dKar%4|B>Ruy ztJ98>+a&8I7GfWyi7aJsq(&!Z{&ln6VN(F%&6jX$QV)r0N3;gNU2$`ht8Ti}W=eZP z3`EY&iWy>YByW;sIVCsGS#ZLm4B~4uvD>Xda(|R{69eH-r+{UM#bKk7Wfk24_1kgu zay@8I-Nctk7bN$TteY4J377zuAr=RW3oI++UiqK+EEVJXJq-}GzX`kIlO%V5teY4J z+1L&&LoC{Ud9bXa+v^FBL*}cnW9}z*4zBLEkF1**2<#sPmLV220SX~f6D(Nc#&wu2aO z&&mbOw|&5J#N%^=6ZXWKm_w;USj-tit0fkEyEo&*igu}^TDF52aIOsm&azF+5sw`Q z8_|*&$Q*VZF63aN6TY8}!tVMQ@X~i-n=mAG^viY-1C9|6!g89Ym?Iv|8N=B5?l^NO zcFgpKq2;a#++OU$drhBKIyAAe9mD`vkt-~i?+7eMJU*)Q#Ev?5=1}M;5GDp(5RXNJ zvo#J|mm|nc=ca}|7djetKKwl+h!GZV@M4BoZ2Q{5vUcvZ^G6s7^~KIjS3q+uA5AK& bBOd0LJmRE!9PEa}!d~L9Ub&Md={HAeBOqg3xY4DF@!rp2YhAH#tZ@fFGfC zbQZx(3hh!Hh4v$~o0He~P41CUlul3U@DC(UF8Aii@w<0PhJ;(;FU!>K`Gae{@@;>x zojqQ-|7f9m{#*a&7f;uhS6^mpz0KjE%$}DUZ(jE|v!y|~wK2^6m%sWe+`SpX^q-%R z_Tu{LM$viKtB(8Mi|*=&>hs?1=6INic`dF}aSiz>ZNv36wwPZ$smI21s>8*-$vO1^ zHV7bq00IagfB*srAb*N_h^VFbqUKo$Zzn4}Xbg$qsPW>bQhfTss%UpU=5=MEA z95a1tFB*5Szms2x+&+>0T0nj{dnW|WUSGB2ED3Mo?DbVU&XVvZ&R$=&<17hp;_UTR zJI<2uCeB`8wc{)aZ{qCrRXfg-@Fvb)U$x^b32)-;^;J90lJF+ZUSGB2ED3Mo?DbVU z&XVvZ&R$=&<17hp;_UTRJI<2uCeB`8wc{)aZ{qCrRXfg-@Fvb)U$x^b32)-;^;J90 zlJF+ZUSGB2ED3Mo?DbVU&XVvZ&R$=&<17hp;_UTRJI<2uCeB`8wc{)aZ{qCrRXfg- z@Fvb)U$x^b32)-;^=-Ac-YFBNC-0O=$Lgi}bUG|LTb=6Xy@&cwnjGcLpnty;g>h1B V62IiYd|@6i50U`$fO+7U2WJZ`YIy(v literal 0 HcmV?d00001 diff --git a/tests/data/lines.vgf b/tests/data/lines.vgf new file mode 100644 index 0000000000000000000000000000000000000000..3b9eadc0f65a111a1744400cf06a88cafedb7e00 GIT binary patch literal 4004 zcmc(ie^6A{6~}iM2_UfY`?s(~geWm0Q4ylC?-)UgqexPvEw-qUuC}5gG_9s7Nf^n9 z{h?0CG^9FJ6oMDXma2Y6>ekUV{a`!FYe_C`em|ad6gF>bI5| zx(HSU3u8KX92T9N#ymt#ewj6R@9+lJ;MZ=oI9%PMGUm|8I`-j6gvblS*fYZx0JYfm z%vaPmNgkt%n2QQtda$3zA@brMO+2&Mnc|3d>|<1JG-c!b+22`?BnbPPC$_^XklG^f z+RL9(KYKe;@H*9tGw5HsG6#|K*TLeIMS>*_Hr()H4PHCZEc{g}8)hk7+g-F#= zyzu%>>SNDmC@g)6gV_bQVD8-gf+Y?9r@aqrR>umRQWs8Ss!IX{mX71~=*#dzkRMik z7Efb5?lj?_L3dmi;^4VYy4wnC`qZNZWTFZ(XV>cIe^Q~Y_4aNRkQz|e1J{KD4Z=e-8%V}H}0RGues z;8>Od?#;P^B@OfVrQZkGzdX-uzi>@WbxD%OpZpB(4!sNB&!*s=`bOE`o~{=2XAW$a z6@v2>gJ4O6Jxh+U279u#X8VQf%FmVa=%ptEzrgPCCh*O!!mh3$>f^aHWAR*>gGrgI zKwIPv?qlwPCmnV#d7E|EH5iNCZ@*=>4z3S-acq}Hwr68!^;PisZ##BO(^DVk=VxTT z4h?gl{_SO$Trm%v{$2r|y7NNQ>Fvlm?1-C=9hp zd}DL4MbD=`=W~53pPD&P^%a3#>qT%raU49SlnPDjyhLotAH|m45v<3SHV^hQ%V$Sk zTqh2_G+g0>H+OsqKEvJEI8sG@6MKN})r(hLtEa1mIk0y56WHh81lPUw@etmyF9xr^XR*F-mz>9R4ovJf=0M#M3=VH5gInq@!IKUf z)^%e;$iG;N4dH2Kd3aALdA{gcg7wL}z_W1=*1h!`IS;Sd{CTM*;-vA@Gr=yg7n~pO z6g=th>LgdZ67R%Xtg9X|%i};Erv0s#2Hv@XbyRn+gS~huHb>6GjAb6;K;4!JTI*Th zmjAxsNr$yj!K}keTVBUYtJ=)+@R_Qtr!Of4FV`l6D?Y*t1KDyO)AvQG8*$R(*&|@H zJEv(s`CbL@tlt3FO|yc;rH6IZp2BC z{C9xOW<5B3dsXnH!%E*{Se_zb`SKj0SMp4pxyC$EOR@Z~*TFS83QMzw_DgCmo)tc@K-vjj|St=Pwcaz<5UUdCYa;Gl25s%)~Rh`X^PW315gdW?D4hx72B z(@U*OS7K4qe6TyX0t*9tsE^0pDJ=64Ck>8(G`>y?)|D}WCmkC4hwwx}I2PvG3cXS{ z&N1=*ky=U)VPUTxY!-cp`2)YCF8QPhE);QVRoAxp7CE#W9%(FO;;cW0>YbN`sf>iBOX8TZo`tVJA_8@=9((rUhAW= ze9-}T?!E8u zu{ZBkZbolnz{^Pj|GgT)5s&$SLzs8^kkzY31?XudYTh5)S|4`1PLDf{t$ z;%4+F2E6tb35Hnw_s`D>R`E2whwPgOlVdXR$X*rrdpcn5i!Id0^W|?GH=`#p;Ox-} z-kZM>9PxNKeJy?%eMD#!Z(q`w=9>uCIb(S5Mfxp~;EMy}?8V;vE$3$RCI;-wYQe3B zet!{1JhM3XyI350w#%uJWFzu5#@Jvs`2U)iuu&fvF?@f`%_*sNgn7(Bi<^)5m z;EZP#`%RTK>=_3EqdV}Mjxw0hkbxtsBjqu!zoj2>f+1FL%9-JB=9d5g{q%R%3zZPM epaPrkE~GJDFW27UOyY3}I!F19Ja(0CVg7%J_vu0a literal 0 HcmV?d00001 diff --git a/tests/data/misc.vgf b/tests/data/misc.vgf new file mode 100644 index 0000000000000000000000000000000000000000..cfce4d9f377343e91bf3d43ecc26cf5b2abaddfe GIT binary patch literal 956 zcmZP&W55C!SD*`qr4|)u=I1Gx>lvEr8KBG3p6}-v?imoQ5SE%;l3%3YUX)mnk(sRE zmS2>cSfW6C>j&NjVHRXJFabFr?Bis+ru5Kz-3%BF6k}vyU{IFDpAaGYyg^tkOP^7 z+bo+6Y=>?YSUN#zph{G;fawuQ4r~+yCmV!@kdirUq9dj>9a?$*hNt?qBN! arJ-hV*&hJ17#NBi6u>ltbFy|LObq}Smvlt{ literal 0 HcmV?d00001 diff --git a/tests/data/sig_airmet.vgf b/tests/data/sig_airmet.vgf new file mode 100644 index 0000000000000000000000000000000000000000..8a23fa9d4e880dd2b206a013913601fecb3e54a2 GIT binary patch literal 1240 zcmZP&W55C!SD*`qr4|)u=I1Gx>lvEq8KBG3p6}-v?imoQ5SE%;l3%3YUX)mnk(sRE zmS2>cSfW6C>j&Njd2R+qAcO*DAPvOK22S$73l3e}D*&UR@~l7_q?ifB_YQXRS5Qy@ zF$n@k1qOy-PuHLzN1zU<9zzQQ14Fbp2a1`XixD)OLNr%VbC9jVkgbTrNG&;lvEq8KBG3p6}-v?imoQ5SE%;l3%3YUX)mnk(sRE zmS2>cSfW6C>j&Nj1#Tur28Mc|Lx2>66a=%LaAMcdJal)@VHgc{CQa^Oqpz_dGnVLiEb{;vj ahvUVe3$;Fn?noI9lVBM2#%Ks&4FLf1`%}{Z literal 0 HcmV?d00001 diff --git a/tests/data/sig_intnl.vgf b/tests/data/sig_intnl.vgf new file mode 100644 index 0000000000000000000000000000000000000000..df506d4470d78670674775df2729d9cb8cad001a GIT binary patch literal 1248 zcmZP&W55C!SD*`qr4|)u=I1Gx>lvEq8KBG3p6}-v?imoQ5SE%;l3%3YUX)mnk(sRE zmS2>cSfW6C>j&NjX>JBaAcO)oAPvOK7EV&`GKao83&Ln5d5~fz5Z~L|*;7G50mLK- z92FQC9DUpzf$Bj3q@K~x!oa}56vRXTpqL4|n3H3$t4qAUUwp7*h^vo}XNYUOzf&;8 zAgm<9IIOJy|8IPB3kr-6!EFWuA$eV_CgT?b*$cw>RSkbxPrs00H((m@4RdFR1bP=& zm_Wr@{eWsi`~!k(9G${yjDcy!phh<@KQC3cMxlmUq2;8qJlDzS%W5Z^dLAdWnKPW^ f-D6;B|IqvSrH8Jw{W)~={Ix^Rce@?>C^#7aHO*x* literal 0 HcmV?d00001 diff --git a/tests/data/sig_nonconv.vgf b/tests/data/sig_nonconv.vgf new file mode 100644 index 0000000000000000000000000000000000000000..f3c0b44e89fe469ac3e8fdce78a99f16fefc6401 GIT binary patch literal 1240 zcmZP&W55C!SD*`qr4|)u=I1Gx>lvEq8KBG3p6}-v?imoQ5SE%;l3%3YUX)mnk(sRE zmS2>cSfW6C>j&Nj8EytfAcO*TAPvOK22PB>6c3%?H-ynpc~&3|Qp^P6dk4GuD<~*{ zm;`~N0t17ef0(PUlWP!E52K-lfq@}foCC#7(8UNEPNIVU|8IN@2@P@z^>Oucb_Kcy z6EF~x*Co+TkaCc{AWVkVVX4J8%pL46TwwwgXY~W7G2OiUyj0zq5dRv58Yo+#h5?AE s1f2LHgq(!ux;k+umV(l}6PwUGQ2IY~KvenA?q}VH4oqf0bTs@P0GV@DYXATM literal 0 HcmV?d00001 diff --git a/tests/data/symbols.vgf b/tests/data/symbols.vgf new file mode 100644 index 0000000000000000000000000000000000000000..7d87f1197bc5926009f9148d398b32093bedf2cf GIT binary patch literal 3008 zcmc)MT}TvB6bJBKKl)H}qe5D<9J4^kwUrDDlAJ>nf{7rRgp!Mx36-%e7AR5Ki%=37 zzKCKkm6F((4;B?2gAxlPFt!Iv(TMWF3<;u_&fR;~yF1g&n=~-{cjrI+_MT<&jw8kK zkHdM#c(J_Jf2O|Sl$M{9yEDgYY?uChiGH}Kv`j0ntqC;vwF7?N>Erb^+WrRrNnb!) z`j7wrpWup%T!VNc&SEIo`UZcvJHtuWoDN59#@-9`7ae+`!x7Pit~sKu7`iAH{6_&h5LiCy#wXO&&YZ^!TcSC_pleJn{i=E`+Ev#Kr75MbrCg3z#8d<8d zx4dI(@Zr9B;51s0N0#cOWi?~=cs2G0JAu<^=^9z8la|$t+iw)%&&n3yG+Izhmg?*+ zPkS%Ucs>KC$*Lwxb<(o>O=QQf!w;j|fYWH{uf>e=9tqK_)r`3!3efQa49$`mUD{vYuxI~uf z?5)ikYjNTETi`TV9b~CaT2?cryN=?w9cO{lXz7z=sZLr}Gp5yl$4T!s;51s$Lze37 ztqt9cIDTXvI89a`S*nwk)r_g__wi-VA>cGwbRnXXmeq{ztNA$O^8lyGdPbJ&?5&iN zKiIpx5;#rPd$Lp~Evp$*{3o!>eGE8F7Hty|o&CDjrsv>Xp9Y*J>oQrYv$s~}-NQ+D z1#p_IZn9J-Evp$5yrVciJOG>~YXw=Vlh&%x{z#43mqUBD+_#gf;&3?iBXAlmUF@~0 zla^f9GLw~9K7~W6UxAaY%<%IBtS3u#k(O9jc>kB{avhw;e$OZ1WNY!+1k%V-owVd{ zRaj!!uve`A8kobe+s<$rt->C9*5P}olZTv9Sn+f&T97mwgQIoL;iN9=IR`Xa7vDpj zJd`EAGi4?J-h$nOWxz>Yw3Q^jdwCB_;s4QMT?tW^bE*;hXM(_Kvi6Xrx)Apl8zCfb(FOsO!5HNEoM4s96BkfWE&y{OmfTPT$jpWGW)2 zwHaQ_=~Qb)EWkoLy#4<>oSmo)N56xV*UtOf@#b~Cc0R7e=H)596jMCr@{a;H4iL;o z%6UvYfkn)bpCc2@X^8Z^$Fn8n5nJu}g&YS2c`@g^i~edswxT6|pfjIJic!^T&^p&; z5XLvg2Z@iuSD~0(C2A22A}T1sh4?5GL5ZMMhyl0Im`h3vd$AK6+aUf4 zRtnoFsD)sY!p=%!Wnm?_-(?5Jux)T)?%SE{FW=2BCkBNhLf4JWOx`c4!l5^lNKPjv z8-Hzny}r1;y0PhH^0{)s_m=(a(Z0%gO9lTRTlSi7|NjmHZTubjO@UN0F?FlIW>-kD zDOI_=g4)0hx0no>Bn~P#gL*4pDhu(~sNM9DN=~Ft^=m9j3LtfYr|1lxbptXY4k|c< z+GPX~sbuVNM1RzZqySR8dpky2u{OacA`U7zgW6?;KS3(ldv~L2qg7G>)`-rU8xa@0 z1ZTjxjAEvbRB|{nq+j9@QULbodayQuyTJrd;-G>vs9i=KrjJx|?08&1oX?R0utwcj zo8U&oK?P?}yNvowAF1TR=a0TGB}oC;qYGngf*&1mP{A40E~75fM=CiVe$v&mZ&Cnu zqTs?!$tNNXDma7MWkhdgzN1Q7zpoWZ0azm#Ah;26^@%K7-{v;Lu^^Rp(Z&8ZWvT5*rt zQ(x04Ccgdu?;xyJ_Mi>#V}t9EE+}sw-)q2Y(@Xk@rXtE!}u}4^4(0i6NA1oppiCY`fiDK zo!e18R@SASyvgFXzTY{NkPI0oKZh~A^E z$8j+-!17(8`6l*!UIjGLE^ObO^rpho=_~~o!FSbf75ICG7^laVa|WVffan;6YqKES zGlSrhL3rLw^o>1^8SH*^9=p?zVV7rTRK++bb_STfdwCcK0|OxZiL?>ZcS}+xwmZD7 zQwomzZtPRx6*vQNWq{}ygqCp-+D1UQ@fL(zJ`7AB#$dYz2dcCAUusflvEq8KBG3p6}-v?imoQ5SE%;l3%3YUX)mnk(sRE zmS2>cSfW6C>j&NjWo~3QumL$hEH2|@apugSFEMH`I=DnH-Xk?9HLn<1Iq_^~PX-1D zh;k-lBSQmIj4#vND$&095Nz3 zj>FRAz)~s53JM;q7+7yuSY#Q8Umvs015$M{x>un z&Htl`VKf9rWC(!rzxinXACYlC>gmxC2=VX?RsbSL1<*)|-e`b~h5%hdAl%i@g~9<# z*jP8*-74IyjBpME6OhTuz`(@eWUx=|(AUG?VKh_;7eXlmNE=8!7#NwtSWv#X0Yk8x zGm4T3)yfCO!AjmEX%3at+UI<~I=T*Up7s8Sho&%q~ z&+~hJ|Mr_NKId!YTMzi8oxk6>eP{Q+-5u`-UansCQuPb>pFaA%UE5N>wK=eT=Z^gy z-GL3=ZJoPz?+Cow(Y?2Af8f!V|Nkq@PfxR~`FHzBX94-UR9f#(kI2*wPT#_D0}d;~duiRh~JtU_zhV3A{q>YyHYK?b%!BfJA`@E+`e zz0d*s-~fc64-UZ*I0k1S2AAL`NWrg`HNyGGNf?3e0sF{ra1H*ktkE;(EAJMiTGiA?8a0%ihc0*~(ml>cu> z`6aBLp5CYy`TJB(f2mp+X;O<<-cY{NXH-^AjanRTRZA>OElK%Qu0F>vd)Tjh#O9Sm zln-)<$=2t=F17fFODd~(S}h_+9^?+D)O_}uH1cWxY~z>C5fC?N%lD6;OAgV`hs3;a>KAB`b)*RJR^yk8i{lF z)PC`lPvSSHB~Ffn-jh4Ar(Y7pPIeB-Wbd^1ym8x7j7hu7h4*bQjTHkz~D1L(~*dF(@Q1j2w8Y<~(L!)NdX48qs& zEqn)P&h{8gKn#*F1!&avWk5%^uL63s{fA|BZ3gm$=r2TnA$kj;L5JP~^yCnIg!VO_N4Cf#P=3^#cCSWFDCNO&f zsvLc{Tx-o}wS`dHTBQomh6|w>J+~N}J}cthw+uacIrak9e)Qa>=(@#dx}|8o`DnWN z?DcsedUGMMrFxAPlQ@|LZb4GXXOJ zGXXOJGXXOJGXXOJGlAI?Q2t{nmCyaXIF+fMIek)9=J0()f7gg?^)zC)a#HKX;x_&% Ua&(PEdwnv#=Oc;gea)(W0et!VtN;K2 literal 0 HcmV?d00001 diff --git a/tests/test_vgf.py b/tests/test_vgf.py new file mode 100644 index 0000000..f368382 --- /dev/null +++ b/tests/test_vgf.py @@ -0,0 +1,616 @@ +# Copyright (c) 2024 Nathan Wendt. +# Distributed under the terms of the BSD 3-Clause License. +# SPDX-License-Identifier: BSD-3-Clause +"""Tests for decoding GEMPAK VGF files.""" + +from pathlib import Path + +import numpy as np + +from gempakio import VectorGraphicFile +from gempakio.decode.vgf import (AdvisoryType, BarbAttribute, HashAttribute, LineAttribute, + Severity, SpecialGeography, VGType) + + +def test_fills(): + """Test decoding of fill colors.""" + expected_fills = [2, 3, 4, 5, 6, 7, 8] + + vgf = Path(__file__).parent / 'data' / 'fills.vgf' + + v = VectorGraphicFile(vgf) + + decoded_fills = [e.filled for e in v.elements] + + assert decoded_fills == expected_fills + + +def test_fronts(): + """Test decoding of front elements.""" + expected_character = ['unspecified', 'forming_suspected', 'diffuse', 'forming_suspected', + 'unspecified', 'diffuse', 'forming_suspected', 'unspecified', + 'diffuse', 'forming_suspected', 'unspecified', 'diffuse', + 'unspecified', 'unspecified', 'position_doubtful', 'unspecified'] + + expected_code = [420, 425, 428, 225, 220, 228, 25, 20, 28, 625, 620, 628, 720, 820, 829, + 940] + + expected_type = ['cold', 'cold','cold', 'warm', 'warm', 'warm', 'stationary', 'stationary', + 'stationary', 'occluded', 'occluded', 'occluded', 'dryline', + 'intertropical', 'intertropical', 'convergence'] + + expected_lat = [np.array([44.39, 46.1, 48.72], dtype='f4'), + np.array([45.52, 46.99, 49.66], dtype='f4'), + np.array([43.2, 45.18, 47.77], dtype='f4'), + np.array([46.75, 43.23, 41.41], dtype='f4'), + np.array([45.88, 41.44, 40], dtype='f4'), + np.array([44.37, 40.28, 38.38], dtype='f4'), + np.array([43.33, 40.27, 37.25], dtype='f4'), + np.array([42.23, 39.2, 36.18], dtype='f4'), + np.array([41.06, 38.08, 35.42, 34.55], dtype='f4'), + np.array([39.77, 36.59, 33.98, 33.56], dtype='f4'), + np.array([38.8, 35.08, 32.99, 32.26], dtype='f4'), + np.array([37.44, 33.88, 32.19, 31.04], dtype='f4'), + np.array([36.1, 33.29, 31.91, 30.31], dtype='f4'), + np.array([34.94, 32.11, 30.09, 28.98], dtype='f4'), + np.array([33.95, 31.08, 28.1], dtype='f4'), + np.array([32.9, 28.97, 27.02], dtype='f4')] + + expected_lon = [np.array([-116.26, -109.71, -103.07], dtype='f4'), + np.array([-116.94, -111.77, -104.71], dtype='f4'), + np.array([-115.27, -108.24, -101.73], dtype='f4'), + np.array([-101.06, -108.17, -114.44], dtype='f4'), + np.array([-99.52, -107.8, -112.03], dtype='f4'), + np.array([-99.12, -105.8, -110.78], dtype='f4'), + np.array([-97.61, -102.49, -109.13], dtype='f4'), + np.array([-96.42, -100.62, -107.37], dtype='f4'), + np.array([-95.14, -98.52, -103.62, -106.13], dtype='f4'), + np.array([-94.18, -97.52, -102.4, -104.39], dtype='f4'), + np.array([-93.06, -96.71, -100.6, -102.41], dtype='f4'), + np.array([-91.72, -95.66, -98.62, -101.39], dtype='f4'), + np.array([-90.63, -94.15, -96.42, -99.77], dtype='f4'), + np.array([-88.81, -92.72, -95.76, -98.85], dtype='f4'), + np.array([-87.5, -90.96, -97.18], dtype='f4'), + np.array([-86, -90.88, -96.05], dtype='f4')] + + vgf = Path(__file__).parent / 'data' / 'fronts.vgf' + + v = VectorGraphicFile(vgf) + + decoded_character = [e.front_character for e in v.elements] + decoded_code = [e.front_code for e in v.elements] + decoded_type = [e.front_type for e in v.elements] + decoded_lat = [e.lat for e in v.elements] + decoded_lon = [e.lon for e in v.elements] + + assert decoded_character == expected_character + assert decoded_code == expected_code + assert decoded_type == expected_type + np.testing.assert_equal(decoded_lat, expected_lat) + np.testing.assert_equal(decoded_lon, expected_lon) + + +def test_jet(): + """Test decoding of jet element.""" + expected_jet = { + 'delete': 0, 'vg_type': 37, 'vg_class': 15, 'filled': 0, 'closed': 0, 'smooth': 2, + 'version': 0, 'group_type': 0, 'group_number': 0, 'major_color': 2, 'minor_color': 2, + 'record_size': 24076, 'min_lat': 42.88, 'min_lon': -112.21, 'max_lat': 44.08, + 'max_lon': -94.33, + 'line_attribute': LineAttribute(line_color=2, number_points=3, line_type=6, stroke=1, + direction=0, size=1.0, width=7, + lat=[42.88, 43.06, 44.08], + lon=[-112.21, -105.11 , -94.33]), + 'number_barbs': 1, + 'barb_attributes': [ + BarbAttribute(wind_color=2, number_wind=1, width=801, size=1.0, wind_type=114, + head_size=0.0, speed=100.0, direction=-96.99, lat=43.06, lon=-105.07, + flight_level_color=2, text_rotation=1367.0, text_size=1.1, + text_type=5, turbulence_symbol=0, font=1, text_flag=1, text_width=1, + text_color=2, line_color=2, fill_color=2, align=0, text_lat=43.06, + text_lon=-105.07, offset_x=0, offset_y=-2, text='FL300') + ], + 'number_hashes': 1, + 'hash_attributes': [ + HashAttribute(wind_color=2, number_wind=1, width=2, size=1.0, wind_type=1, + head_size=0.0, speed=0.0, direction=-89.47, lat=42.86, lon=-110.08) + ] + } + + vgf = Path(__file__).parent / 'data' / 'jet.vgf' + + v = VectorGraphicFile(vgf) + + jet = v.elements[0] + + assert jet.__dict__ == expected_jet + + +def test_lines(): + """Test decoding of line elements.""" + expected_style = ['solid', 'long_dash', 'fancy_arrow', 'pointed_arrow', 'filled_arrow', + 'line_circle_line', 'line_fill_circle_line', 'alt_angle_ticks', + 'tick_marks', 'scallop', 'zigzag', 'sine_curve', 'ball_chain', + 'box_circles', 'filled_circles', 'line_x_line', 'two_x', 'box_x', + 'line_caret_line2', 'line_caret_line1', 'fill_circle_x', 'arrow_dashed', + 'fill_arrow_dash', 'streamline', 'double_line', 'short_dashed', + 'medium_dashed', 'long_dash_short_dash', 'long_dash', + 'long_dash_three_short_dash', 'long_dash_dot', 'long_dash_three_dot', + 'extra_long_dash_two_dot', 'dotted', 'kink_line1', 'kink_line2', + 'z_line'] + + expected_type = [1, 5, 13, 4, 6, 16, 10, 5, 11, 3, 2, 19, 1, 7, 9, 12, 8, 15, 18, 17, 14, + 20, 21, 22, 23, 2, 3, 4, 5, 6, 7, 8, 9, 10, 24, 25, 26] + + expected_color = [3, 5, 17, 18, 18, 17, 2, 7, 17, 18, 5, 2, 3, 2, 2, 17, 2, 17, 1, 1, 17, + 18, 18, 7, 2, 5, 5, 5, 5, 5, 5, 5, 5, 5, 2, 2, 2] + + expected_lat = [np.array([47.07, 48.35, 47.42, 46.03, 46.51, 47.07], dtype='f4'), + np.array([47.74, 48.69, 46.44, 46.44, 47.74], dtype='f4'), + np.array([44.14, 46.3 , 47.83, 48.46], dtype='f4'), + np.array([42.8, 44.63, 46.51], dtype='f4'), + np.array([41.75, 43.91, 45.05, 46.25], dtype='f4'), + np.array([40.68, 43.21, 44.79], dtype='f4'), + np.array([39.75, 41.01, 42.77, 44.22], dtype='f4'), + np.array([33.35, 35.43, 37.46, 39.05], dtype='f4'), + np.array([32, 34.07, 36.45, 38.29], dtype='f4'), + np.array([31.49, 33.13, 35.67, 37.49, 38.89], dtype='f4'), + np.array([30.43, 32.35, 34.68, 36.14, 37.78, 38.71], dtype='f4'), + np.array([30.24, 32.23, 34.46, 36.42, 37.77, 38.82], dtype='f4'), + np.array([30.22, 33.27, 34.9, 36.88, 38.74], dtype='f4'), + np.array([29.8, 32.46, 35.12, 37.11, 38.22], dtype='f4'), + np.array([29.11, 31.96, 34.34, 36.49, 37.95], dtype='f4'), + np.array([29.44, 32.46, 35.51, 37.16, 38.14], dtype='f4'), + np.array([28.8, 31.27, 33.85, 35.73, 36.94], dtype='f4'), + np.array([28.23, 30.75, 32.83, 35.23, 36.71], dtype='f4'), + np.array([28.02, 30.89, 33.85, 35.25, 36.52], dtype='f4'), + np.array([28.03, 31.79, 34.1, 35.61, 36.85], dtype='f4'), + np.array([27.59, 30.75, 33.24, 34.87, 36], dtype='f4'), + np.array([27.57, 30.26, 32.74, 34.37, 35.43], dtype='f4'), + np.array([26.91, 29.62, 31.92, 33.43, 34.39], dtype='f4'), + np.array([26.84, 29.69, 32.54, 33.81], dtype='f4'), + np.array([26.23, 29.35, 32.41, 33.8 ], dtype='f4'), + np.array([39.43, 40.88, 42.79, 45.21], dtype='f4'), + np.array([38.6 , 39.8, 41.49, 43.96], dtype='f4'), + np.array([38.09, 39.87, 41.88, 43.15], dtype='f4'), + np.array([37.6, 39.18, 41.16, 42.3 ], dtype='f4'), + np.array([37.14, 38.68, 40.31, 41.48], dtype='f4'), + np.array([36.44, 38.02, 39.63, 40.7], dtype='f4'), + np.array([35.3, 38.14, 39.76], dtype='f4'), + np.array([34.57, 36.27, 38.35, 39.52], dtype='f4'), + np.array([33.4, 34.93, 37.19, 38.84], dtype='f4'), + np.array([37.86, 40.68], dtype='f4'), + np.array([39.99, 42.45], dtype='f4'), + np.array([42.81, 41.18, 39.98], dtype='f4')] + + expected_lon = [ + np.array([-121.06, -119.31, -118.09, -120.04, -120.87, -121.06],dtype='f4'), + np.array([-113.58, -108.01, -110.21, -113.02, -113.58], dtype='f4'), + np.array([-108.63, -103.41, -102.13, -102.34], dtype='f4'), + np.array([-106.99, -102.73, -99.56], dtype='f4'), + np.array([-105.27, -100.43, -98.16, -96.73], dtype='f4'), + np.array([-103.64, -98.01, -95.44], dtype='f4'), + np.array([-102.66, -99.05, -95.96, -93.87], dtype='f4'), + np.array([-117.15, -115.95, -116.56, -117.14], dtype='f4'), + np.array([-114.46, -113.63, -113.4, -113.77], dtype='f4'), + np.array([-112.5, -112.13, -111.94, -111.96, -112.25], dtype='f4'), + np.array([-110.05, -109.57, -109.66, -109.76, -110.14, -110.37], dtype='f4'), + np.array([-107.6, -107.33, -107.24, -107.2, -107.42, -107.77], dtype='f4'), + np.array([-105.72, -105.44, -105.23, -104.98, -105.13], dtype='f4'), + np.array([-104.02, -103.18, -103.08, -102.95, -103.1 ], dtype='f4'), + np.array([-102.7, -101.51, -101.16, -100.75, -100.87], dtype='f4'), + np.array([-100.84, -99.61, -98.65, -98.71, -98.66], dtype='f4'), + np.array([-99.39, -98.2, -97.41, -97.09, -96.96], dtype='f4'), + np.array([-97.77, -96.24, -95.57, -95.29, -95.14], dtype='f4'), + np.array([-95.38, -94.05, -93.22, -93.14, -93.12], dtype='f4'), + np.array([-93.28, -91.54, -91.39, -91.33, -91.27], dtype='f4'), + np.array([-90.9, -89.38, -88.37, -87.98, -88.11], dtype='f4'), + np.array([-89.24, -87.89, -86.93, -86.41, -86.25], dtype='f4'), + np.array([-87.41, -86.46, -85.83, -85.3, -85.06], dtype='f4'), + np.array([-85.45, -84.8, -83.98, -83.96], dtype='f4'), + np.array([-83.23, -82.49, -82.35, -82.08], dtype='f4'), + np.array([-97.78, -94.2, -92.43, -90.99], dtype='f4'), + np.array([-95.11, -92.55, -90.97, -89.06], dtype='f4'), + np.array([-93.12, -89.84, -87.63, -86.83], dtype='f4'), + np.array([-90.99, -88.1, -86.08, -85.29], dtype='f4'), + np.array([-88.81, -86, -84.38, -83.49], dtype='f4'), + np.array([-86.01, -83.57, -82.35, -81.78], dtype='f4'), + np.array([-83.01, -80.67, -79.9 ], dtype='f4'), + np.array([-81.27, -79.73, -78.14, -77.62], dtype='f4'), + np.array([-119.12, -118, -118.21, -118.74], dtype='f4'), + np.array([-122.27, -121.27], dtype='f4'), + np.array([-124.43, -121.66], dtype='f4'), + np.array([-118.1 , -111.86, -106.45], dtype='f4') + ] + + vgf = Path(__file__).parent / 'data' / 'lines.vgf' + + v = VectorGraphicFile(vgf) + + decoded_style = [e.line_style for e in v.elements] + decoded_type = [e.line_type for e in v.elements] + decoded_color = [e.major_color for e in v.elements] + decoded_lat = [e.lat for e in v.elements] + decoded_lon = [e.lon for e in v.elements] + + assert decoded_style == expected_style + assert decoded_type == expected_type + assert decoded_color == expected_color + np.testing.assert_equal(decoded_lat, expected_lat) + np.testing.assert_equal(decoded_lon, expected_lon) + + +def test_markers(): + """Test decoding of marker elements.""" + expected_code = [10, 5, 16, 11] + expected_lat = [47.42, 44.16, 43.68, 47.17] + expected_lon = [-119.59, -120.34, -114.85, -109.22] + + vgf = Path(__file__).parent / 'data' / 'misc.vgf' + + v = VectorGraphicFile(vgf) + + markers = v.filter_elements(vg_type=19) + + decoded_code = [e.symbol_code for e in markers] + decoded_lat = [e.lat for e in markers] + decoded_lon = [e.lon for e in markers] + + assert decoded_code == expected_code + assert decoded_lat == expected_lat + assert decoded_lon == expected_lon + + +def test_sigmet_airmet(): + """Test decoding AIRMET element.""" + expected_airmet = { + 'delete': 0, 'vg_type': 31, 'vg_class': 11, 'filled': 0, 'closed': 1, 'smooth': 0, + 'version': 1, 'group_type': 0, 'group_number': 0, 'major_color': 3, 'minor_color': 3, + 'record_size': 816, 'min_lat': 40.0, 'min_lon': -104.87, 'max_lat': 40.0, + 'max_lon': -104.87, 'subtype': 0, 'number_points': 5, 'line_type': 1, 'line_width': 2, + 'side_of_line': 0, 'area': 'KSFO', 'flight_info_region': '', 'status': 'KSFO', + 'distance': 10.0, 'message_id': 'SIERRA', 'sequence_number': 0, 'start_time': '180010', + 'end_time': '180410', 'remarks': '', 'sonic': -9999, 'phenomena': 'IFR', + 'phenomena2': 'IFR', 'phenomena_name': '-', 'phenomena_lat': '', 'phenomena_lon': '', + 'pressure': -9999, 'max_wind': -9999, 'free_text': '', 'trend': '', 'movement': 'MVG', + 'type_indicator': -9999, 'type_time': '', 'flight_level': -9999, 'speed': 5, + 'direction': 'N', 'tops': '-none-|TO| |-none-| |', 'forecaster': '', + 'lat': np.array([41.852974, 44.174183, 43.476295, 41.47952, 39.995544], dtype='f4'), + 'lon': np.array([-104.86926, -101.10242, -98.236664, -98.69969, -104.125145], + dtype='f4') + } + + vgf = Path(__file__).parent / 'data' / 'sig_airmet.vgf' + + v = VectorGraphicFile(vgf) + + jet = v.elements[0] + + np.testing.assert_equal(jet.__dict__, expected_airmet) + + +def test_sigmet_ccf(): + """Test decoding CCF element.""" + expected_ccf = { + 'delete': 0, 'vg_type': 32, 'vg_class': 11, 'filled': 2, 'closed': 1, 'smooth': 0, + 'version': 0, 'group_type': 127, 'group_number': 1, 'major_color': 26, + 'minor_color': 26, 'record_size': 1480, 'min_lat': 33.79, 'min_lon': -110.87, + 'max_lat': 33.79, 'max_lon': -110.87,'subtype': 0, 'number_points': 8, + 'coverage': 3, 'storm_tops': 1, 'probability': 1, 'growth': 2, 'speed': 45.0, + 'direction': 90.0, 'text_lat': 43.82, 'text_lon': -112.48, 'arrow_lat': 38.8, + 'arrow_lon': -99.5, 'high_fill': 6, 'med_fill': 4, 'low_fill': 2, 'line_type': 1, + 'arrow_size': 1.0, 'rotation': 0.0, 'text_size': 0.0, 'text_type': 0, + 'turbulence_symbol': 0, 'font': 1, 'text_flag': 2, 'width': 1, 'fill_color': 0, + 'align': -1, 'offset_x': 0, 'offset_y': 0, 'text': '', + 'text_layout': 'IBDR|221;TEXT|TOPS::ETR;TEXT|GWTH::GWTH;TEXT|CONF::CONF;T', + 'lat': np.array([39.7, 43.65, 43.86, 41.52, 37.4, 33.79, 34, 36.71], dtype='f4'), + 'lon': np.array([-110.87, -105.94, -97.59, -90.56, -87.36, -94.02, -104.24, + -110.05], dtype='f4') + } + + vgf = Path(__file__).parent / 'data' / 'sig_ccf.vgf' + + v = VectorGraphicFile(vgf) + + ccf = v.elements[0] + + np.testing.assert_equal(ccf.__dict__, expected_ccf) + + +def test_sigmet_international(): + """Test decoding international SIGMET.""" + expected_intnl = { + 'delete': 0, 'vg_type': 27, 'vg_class': 11, 'filled': 0, 'closed': 1, 'smooth': 0, + 'version': 1, 'group_type': 0, 'group_number': 0, 'major_color': 6, 'minor_color': 6, + 'record_size': 824, 'min_lat': 38.57, 'min_lon': -122.63, 'max_lat': 38.57, + 'max_lon': -122.63, 'subtype': 0, 'number_points': 6, 'line_type': 1, 'line_width': 2, + 'side_of_line': 0, 'area': 'KKCI', 'flight_info_region': '', 'status': 'KKCI', + 'distance': 10.0, 'message_id': 'ALFA', 'sequence_number': 1, 'start_time': '180005', + 'end_time': '180405', 'remarks': 'BASED_ON_SATELLITE_OBS', 'sonic': -9999, + 'phenomena': 'FRQ_TS', 'phenomena2': 'FRQ_TS', 'phenomena_name': '-', + 'phenomena_lat': '', 'phenomena_lon': '', 'pressure': -9999, 'max_wind': -9999, + 'free_text': '', 'trend': 'INTSF', 'movement': 'MVG', 'type_indicator': -9999, + 'type_time': '', 'flight_level': -9999, 'speed': 5, 'direction': 'N', + 'tops': 'TOPS|ABV|30000|-none-| |', 'forecaster': '', + 'lat': np.array([41.1635, 44.738934, 47.12407, 41.649994, 39.819687, 38.569443], + dtype='f4'), + 'lon': np.array([-122.63099, -119.81144, -107.51364, -108.90593, -115.86577, + -120.534325], dtype='f4') + } + + vgf = Path(__file__).parent / 'data' / 'sig_intnl.vgf' + + v = VectorGraphicFile(vgf) + + intnl = v.elements[0] + + np.testing.assert_equal(intnl.__dict__, expected_intnl) + + +def test_sigmet_nonconvective(): + """Test decoding of nonconvective SIGMET.""" + expected_nonconv = { + 'delete': 0, 'vg_type': 28, 'vg_class': 11, 'filled': 0, 'closed': 1, 'smooth': 0, + 'version': 1, 'group_type': 0, 'group_number': 0, 'major_color': 7, 'minor_color': 7, + 'record_size': 816, 'min_lat': 32.49, 'min_lon': -100.03, 'max_lat': 32.49, + 'max_lon': -100.03, 'subtype': 0, 'number_points': 5, 'line_type': 1, 'line_width': 2, + 'side_of_line': 0, 'area': 'KSFO', 'flight_info_region': '', 'status': 'KSFO', + 'distance': 10.0, 'message_id': 'NOVEMBER', 'sequence_number': 1, + 'start_time': '180010', 'end_time': '180410', 'remarks': '', 'sonic': -9999, + 'phenomena': 'TURBULENCE', 'phenomena2': 'TURBULENCE', 'phenomena_name': '-', + 'phenomena_lat': '', 'phenomena_lon': '', 'pressure': -9999, 'max_wind': -9999, + 'free_text': '', 'trend': '', 'movement': 'MVG', 'type_indicator': -9999, + 'type_time': '', 'flight_level': -9999, 'speed': 5, 'direction': 'N', + 'tops': '-none-|TO| |-none-| |', 'forecaster': '', + 'lat': np.array([35.586006, 36.903584, 34.845173, 32.494267, 33.518486], dtype='f4'), + 'lon': np.array([-100.02967, -96.04128 , -93.95028, -96.28716, -98.671616], dtype='f4') + } + + vgf = Path(__file__).parent / 'data' / 'sig_nonconv.vgf' + + v = VectorGraphicFile(vgf) + + nonconv = v.elements[0] + + np.testing.assert_equal(nonconv.__dict__, expected_nonconv) + + +def test_symbols(): + """Test decoding of symbol elements.""" + expected_code = [12, 13, 9, 10, 45, 10, 51, 56, 25, 61, 71, 26, 63, 73, 27, 65, 75, 28, + 80, 85, 32, 95, 105, 33, 66, 79, 34, 9, 38, 39, 5, 40, 35, 47] + + expected_lat = [47.29, 47.28, 47.29, 46.22, 44.2, 43.21, 43.89, 43.39, 43.0, 44.38, 42.12, + 40.38, 40.18, 40.39, 40.85, 43.22, 40.53, 39.77, 39.1, 38.9, 38.64, 38.39, + 37.58, 37.61, 36.78, 34.3, 34.55, 35.59, 34.83, 35.83, 30.84, 35.23, 35.74, + 32.47] + + expected_lon = [-120.05, -110.51, -100.84, -94.73, -89.63, -84.92, -121.18, -114.09, + -107.4, -100.77, -93.53, -89.18, -86.38, -83.2, -77.75, -75.31, -122.37, + -116.6, -112.09, -106.68, -98.31, -92.69, -84.51, -78.55, -119.83, -112.05, + -106.88, -97.55, -92.55, -86.59, -103.83, -101.7, -79.35, -86.95] + + vgf = Path(__file__).parent / 'data' / 'symbols.vgf' + + v = VectorGraphicFile(vgf) + + decoded_code = [e.symbol_code for e in v.elements] + decoded_lat = [e.lat for e in v.elements] + decoded_lon = [e.lon for e in v.elements] + + assert decoded_code == expected_code + assert decoded_lat == expected_lat + assert decoded_lon == expected_lon + + +def test_tca(): + """Test decoding of TCA element.""" + expected_tca = { + 'delete': 0, 'vg_type': 39,'vg_class': 15, 'filled': 0, 'closed': 0, 'smooth': 0, + 'version': 0, 'group_type': 8, 'group_number': 0, 'major_color': 0, 'minor_color': 0, + 'record_size': 409, 'min_lat': 29.48, 'min_lon': -93.3, 'max_lat': 29.78, + 'max_lon': -91.29, 'storm_number': 1, 'issue_status': 'T', 'basin': 0, + 'advisory_number': 1, 'storm_name': 'Suss', 'storm_type': 0, + 'valid_time': '240617/0000', 'timezone': 'EDT', 'text_lat': 25.799999, + 'text_lon': -80.400002, 'text_font': 1, 'text_size': 1.0, 'text_width': 3, + 'number_ww': 1, + 'ww': [ + { + 'severity': Severity(1), + 'advisory_type': AdvisoryType(1), + 'special_geography': SpecialGeography(0), + 'number_breaks': 2, + 'break_points': [ + [29.780001, -93.300003, 'CAMERON'], + [29.48, -91.290001, 'MORGAN_CITY'] + ] + } + ] + } + + vgf = Path(__file__).parent / 'data' / 'tca.vgf' + + v = VectorGraphicFile(vgf) + + jet = v.elements[0] + + assert jet.__dict__ == expected_tca + + +def test_text(): + """Test the decoding of text elements.""" + expected_text = ['Test', 'Test', 'Test', 'Test', 'Test', 'Test', 'Test', 'Test', 'Test', + 'Test'] + + expected_type = [0, 11, 4, 5, 14, 13, 3, 2, 1, 10] + + expected_color = [20, 6, 2, 4, 30, 7, 19, 12, 19, 3] + + expected_lat = [43.55, 42.57, 39.7, 36.69, 38.76, 42.12, 47.48, 47.0, 43.66, 39.8] + + expected_lon = [-121.61, -106.06, -117.46, -118.08, -104.56, -100.29, -99.72, -108.78, + -114.15, -110.9] + + vgf = Path(__file__).parent / 'data' / 'text.vgf' + + v = VectorGraphicFile(vgf) + + decoded_text = [e.text for e in v.elements] + decoded_type = [e.text_type for e in v.elements] + decoded_color = [e.text_color for e in v.elements] + decoded_lat = [e.lat for e in v.elements] + decoded_lon = [e.lon for e in v.elements] + + assert decoded_text == expected_text + assert decoded_type == expected_type + assert decoded_color == expected_color + assert decoded_lat == expected_lat + assert decoded_lon == expected_lon + + +def test_tracks(): + """Test decoding of storm track elements.""" + expected_track = { + 'delete': 0, 'vg_type': 26, 'vg_class': 10, 'filled': 0, 'closed': 0, 'smooth': 0, + 'version': 1, 'group_type': 0, 'group_number': 0, 'major_color': 2, 'minor_color': 32, + 'record_size': 1400, 'min_lat': 37.4, 'min_lon': -97.27, 'max_lat': 37.4, + 'max_lon': -97.27, 'track_type': 0, 'total_points': 5, 'initial_points': 2, + 'initial_line_type': 1, 'extrapolated_line_type': 2, 'initial_mark_type': 20, + 'extrapolated_mark_type': 20, 'line_width': 1, 'speed': 6.03, 'direction': 141.85, + 'increment': 60, 'skip': 0, 'font': 21, 'font_flag': 2, 'font_size': 1.0, + 'times': ['240422/2300', '240423/0000', '240423/0100', '240423/0200', '240423/0300'], + 'lat': np.array([38.03, 37.87, 37.72, 37.56, 37.4], dtype='f4'), + 'lon': np.array([-97.27, -97.12, -96.97, -96.83, -96.68], dtype='f4') + } + + vgf = Path(__file__).parent / 'data' / 'tracks.vgf' + + v = VectorGraphicFile(vgf) + + decoded_track = v.elements[0].__dict__ + + np.testing.assert_equal(decoded_track, expected_track) + + +def test_volcano(): + """Test decoding of volcano and ash elements.""" + expected_volcano = { + 'delete': 0, 'vg_type': 35, 'vg_class': 11, 'filled': 0, 'closed': 0, 'smooth': 0, + 'version': 0, 'group_type': 0, 'group_number': 0, 'major_color': 6, 'minor_color': 6, + 'record_size': 5916, 'min_lat': 46.2, 'min_lon': -122.18, 'max_lat': 46.2, + 'max_lon': -122.18, 'name': 'St._Helens', 'code': 201.0, 'size': 2.0, 'width': 2, + 'number': '321050', 'location': 'N4620W12218', 'area': 'US-Washington', + 'origin_station': 'KNES', 'vaac': 'WASHINGTON', 'wmo_id': 'XX', 'header_number': '20', + 'elevation': '8363', 'year': '2024', 'advisory_number': '001', 'correction': '', + 'info_source': 'GOES-17.', 'additional_source': 'TEST', 'aviation_color': '', + 'details': 'TEST', 'obs_date': 'NIL', 'obs_time': 'NIL', 'obs_ash': '', + 'forecast_6hr': '18/0500Z', 'forecast_12hr': '18/1100Z', 'forecast_18hr': '18/1700Z', + 'remarks': 'THIS IS A TEST.', 'next_advisory': '', 'forecaster': 'WENDT', + 'lat': 46.2, 'lon': -122.18, 'offset_x': 0, 'offset_y': 0 + } + + expected_ash = { + 'delete': 0, 'vg_type': 36, 'vg_class': 11, 'filled': 5, 'closed': 1, 'smooth': 0, + 'version': 0, 'group_type': 0, 'group_number': 0, 'major_color': 2, 'minor_color': 5, + 'record_size': 520, 'min_lat': 44.19, 'min_lon': -122.88, 'max_lat': 44.19, + 'max_lon': -122.88, 'subtype': 0, 'number_points': 10, 'distance': 0.0, + 'forecast_hour': 0, 'line_type': 1, 'line_width': 2, 'side_of_line': 0, 'speed': 0.0, + 'speeds': '25', 'direction': '270', 'flight_level1': 'SFC', 'flight_level2': '30000', + 'rotation': 0.0, 'text_size': 1.1, 'text_type': 4, 'turbulence_symbol': 0, 'font': 1, + 'text_flag': 2, 'width': 1, 'text_color': 5, 'line_color': 5, 'fill_color': 32, + 'align': 0, 'text_lat': 0.0, 'text_lon': 0.0, 'offset_x': 0, 'offset_y': 0, + 'text': '', + 'lat': np.array([45.84, 46.33, 46.55, 46.6, 46.78, 48.56, 48.25, 46.51, 44.19, + 44.19], dtype='f4'), + 'lon': np.array([-122.66, -122.88, -122.22, -121.36, -120.48, -116.92, -112.56, + -113.2, -117.89, -121.07], dtype='f4')} + + vgf = Path(__file__).parent / 'data' / 'volcano.vgf' + + v = VectorGraphicFile(vgf) + + volcano = v.filter_elements(vg_type=VGType.volcano.value)[0] + ash = v.filter_elements(vg_type=VGType.ash_cloud.value)[0] + + assert volcano.__dict__ == expected_volcano + np.testing.assert_equal(ash.__dict__, expected_ash) + + +def test_watch(): + """Test decoding of watch box elements.""" + expected_watch = { + 'delete': 0, 'vg_type': 6, 'vg_class': 2, 'filled': 1, 'closed': 0, 'smooth': 0, + 'version': 6, 'group_type': 0, 'group_number': 0, 'major_color': 2, 'minor_color': 2, + 'record_size': 5736, 'min_lat': 32.37, 'min_lon': -101.2, 'max_lat': 32.37, + 'max_lon': -101.2, 'number_points': 8, 'style': 4, 'shape': 3, 'marker_type': 1, + 'marker_size': 1.0, 'marker_width': 3, 'anchor0_station': 'CDS', 'anchor0_lat': 34.43, + 'anchor0_lon': -100.28, 'anchor0_distance': 115, 'anchor0_direction': 'S', + 'anchor1_station': 'AVK', 'anchor1_lat': 36.77, 'anchor1_lon': -98.67, + 'anchor1_distance': 5, 'anchor1_direction': 'E', 'status': 0, 'number': -9999, + 'issue_time': '', 'expire_time': '', 'watch_type': 7, 'severity': 0, 'timezone': '', + 'max_hail': '', 'max_wind': '', 'max_tops': '', 'mean_storm_speed': '', + 'mean_storm_direction': '', 'states': 'KS OK TX', 'adjacent_areas': '', + 'replacing': '', 'forecaster': '', 'filename': '', 'issue_flag': 0, + 'wsm_issue_time': '', 'wsm_expire_time': '', 'wsm_reference_direction': '', + 'wsm_recent_from_line': '', 'wsm_md_number': '', 'wsm_forecaster': '', + 'number_counties': 57, 'plot_counties': 1, + 'county_fips': np.array([20025, 20033, 40003, 40009, 40011, 40015, 40017, 40031, 40033, + 40039, 40043, 40045, 40047, 40051, 40055, 40057, 40059, 40065, + 40073, 40075, 40093, 40129, 40141, 40149, 40151, 40153, 48009, + 48023, 48059, 48075, 48087, 48101, 48107, 48125, 48129, 48151, + 48155, 48169, 48191, 48197, 48207, 48211, 48253, 48263, 48269, + 48275, 48295, 48345, 48415, 48417, 48433, 48441, 48447, 48483, + 48485, 48487, 48503], dtype='i4'), + 'county_lat': np.array([37.24, 37.19, 36.72, 35.27, 35.88, 35.18, 35.54, 34.65, 34.3, + 35.64, 36, 36.21, 36.38, 35.02, 34.92, 34.74, 36.78, 34.53, + 35.93, 34.96, 36.31, 35.7, 34.37, 35.29, 36.79, 36.41, 33.62, + 33.62, 32.3, 34.53, 34.95, 34.06, 33.62, 33.61, 34.97, 32.75, + 34.03, 33.18, 34.53, 34.28, 33.18, 35.83, 32.74, 33.2, 33.62, + 33.6, 36.28, 34.07, 32.74, 32.73, 33.17, 32.3, 33.18, 35.41, + 33.99, 34.06, 33.17], dtype='f4'), + 'county_lon': np.array([ -99.83, -99.27, -98.29, -99.7 , -98.44, -98.39, -97.97, + -98.43, -98.38, -99, -99.03, -99.73, -97.79, -97.88, -99.52, + -99.83, -99.64, -99.26, -97.91, -99.1 , -98.54, -99.73, -98.92, + -98.99, -98.93, -99.23, -98.7 , -99.21, -99.38, -100.22, + -100.24, -100.22, -101.3 , -100.77, -100.84, -100.42, -99.93, + -101.31, -100.68, -99.72, -99.74, -100.26, -99.87, -100.83, + -100.26, -99.74, -100.27, -100.77, -100.91, -99.35, -100.24, + -99.89, -99.22, -100.3 , -98.72, -99.18, -98.68], dtype='f4'), + 'lat': np.array([32.765636, 33.15998, 35.159996, 37.159996, 36.76996, 36.37, 34.37, + 32.36999, 32.765636], dtype='f4'), + 'lon': np.array([-100.27998, -101.19998, -100.37998, -99.54999, -98.57964 , + -97.61999, -98.48998 , -99.359985, -100.27998 ], dtype='f4'), + 'marker_name': 'plus_sign' + } + + vgf = Path(__file__).parent / 'data' / 'watch.vgf' + + v = VectorGraphicFile(vgf) + + decoded_watch = v.elements[0].__dict__ + + np.testing.assert_equal(decoded_watch, expected_watch) + + +def test_wind_vectors(): + """Test decoding of wind vector elements.""" + expected_direction = [185.0, 78.86, 227.17] + expected_speed = [25.0, 100.0, 0.0] + expected_lat = [38.06, 38.57, 42.39] + expected_lon = [-106.31, -100.48, -98.87] + + vgf = Path(__file__).parent / 'data' / 'misc.vgf' + + v = VectorGraphicFile(vgf) + + wind = v.filter_elements(vg_class=6) + + decoded_direction = [e.direction for e in wind] + decoded_speed = [e.speed for e in wind] + decoded_lat = [e.lat for e in wind] + decoded_lon = [e.lon for e in wind] + + assert decoded_direction == expected_direction + assert decoded_speed == expected_speed + assert decoded_lat == expected_lat + assert decoded_lon == expected_lon