277 @spec/@type annotations added to 58 files covering all public APIs: contexts (propagation, radio, weather, terrain, beacons, commercial), GRIB2 decoders, terrain analysis, duct detection, rain scatter, CSV/ADIF import, weather clients, and all Ecto schemas. Dialyzer passes with 0 errors.
286 lines
8 KiB
Elixir
286 lines
8 KiB
Elixir
defmodule Microwaveprop.Weather.Grib2.Section do
|
|
@moduledoc false
|
|
|
|
@doc """
|
|
Parse a single GRIB2 message binary into its component sections.
|
|
|
|
Returns `{:ok, %{grid_params, product, packing_params, data, bitmap}}`.
|
|
"""
|
|
@spec parse_message(binary()) ::
|
|
{:ok,
|
|
%{
|
|
grid_params: map(),
|
|
product: %{var: String.t(), level: String.t()},
|
|
packing_params: map(),
|
|
data: binary(),
|
|
bitmap: :none | :present
|
|
}}
|
|
| {:error, String.t()}
|
|
def parse_message(<<"GRIB", _reserved::16, _discipline::8, 2::8, total_length::64-big, rest::binary>> = msg) do
|
|
discipline = :binary.at(msg, 6)
|
|
# Skip indicator section (16 bytes), parse remaining sections
|
|
message_body = binary_part(rest, 0, total_length - 16)
|
|
parse_sections(message_body, %{discipline: discipline})
|
|
end
|
|
|
|
def parse_message(<<"GRIB", _reserved::16, _discipline::8, edition::8, _::binary>>) do
|
|
{:error, "unsupported GRIB edition #{edition}"}
|
|
end
|
|
|
|
def parse_message(_), do: {:error, "not a GRIB2 message"}
|
|
|
|
@doc """
|
|
Identify variable name from discipline, parameter category, and parameter number.
|
|
"""
|
|
@spec identify_var(non_neg_integer(), non_neg_integer(), non_neg_integer()) :: String.t()
|
|
def identify_var(0, 0, 0), do: "TMP"
|
|
def identify_var(0, 0, 6), do: "DPT"
|
|
def identify_var(0, 3, 0), do: "PRES"
|
|
def identify_var(0, 3, 5), do: "HGT"
|
|
def identify_var(0, 3, 18), do: "HPBL"
|
|
def identify_var(0, 1, 0), do: "SPFH"
|
|
def identify_var(0, 1, 3), do: "PWAT"
|
|
def identify_var(0, 1, 8), do: "APCP"
|
|
def identify_var(0, 2, 2), do: "UGRD"
|
|
def identify_var(0, 2, 3), do: "VGRD"
|
|
def identify_var(0, 6, 1), do: "TCDC"
|
|
def identify_var(0, 19, 11), do: "TKE"
|
|
def identify_var(_discipline, cat, num), do: "#{cat}:#{num}"
|
|
|
|
@doc """
|
|
Identify level string from surface type and scaled value.
|
|
"""
|
|
@spec identify_level(non_neg_integer(), non_neg_integer()) :: String.t()
|
|
def identify_level(1, _value), do: "surface"
|
|
def identify_level(100, value_pa), do: "#{div(value_pa, 100)} mb"
|
|
def identify_level(103, value_m), do: "#{value_m} m above ground"
|
|
|
|
def identify_level(10, _value), do: "entire atmosphere"
|
|
def identify_level(105, level), do: "#{level} hybrid level"
|
|
def identify_level(200, _value), do: "entire atmosphere (considered as a single layer)"
|
|
|
|
def identify_level(type, value), do: "unknown:#{type}:#{value}"
|
|
|
|
@doc """
|
|
Decode a 16-bit sign-magnitude integer.
|
|
"""
|
|
@spec sign_magnitude_16(<<_::16>>) :: integer()
|
|
def sign_magnitude_16(<<0::1, value::15>>), do: value
|
|
def sign_magnitude_16(<<1::1, value::15>>), do: -value
|
|
|
|
# --- Private section parsing ---
|
|
|
|
defp parse_sections(<<"7777", _rest::binary>>, acc) do
|
|
build_result(acc)
|
|
end
|
|
|
|
defp parse_sections(<<length::32-big, section_num::8, rest::binary>>, acc) when length > 5 do
|
|
body_length = length - 5
|
|
|
|
if body_length <= byte_size(rest) do
|
|
<<body::binary-size(body_length), remaining::binary>> = rest
|
|
acc = parse_section(section_num, body, acc)
|
|
parse_sections(remaining, acc)
|
|
else
|
|
# Truncated section — return what we have
|
|
build_result(acc)
|
|
end
|
|
end
|
|
|
|
defp parse_sections(<<>>, acc) do
|
|
build_result(acc)
|
|
end
|
|
|
|
# Trailing padding or short garbage — return what we have
|
|
defp parse_sections(_other, acc) do
|
|
build_result(acc)
|
|
end
|
|
|
|
defp parse_section(1, _body, acc) do
|
|
# Section 1 (Identification) — we don't need anything from it
|
|
acc
|
|
end
|
|
|
|
defp parse_section(3, body, acc) do
|
|
<<
|
|
_source::8,
|
|
_num_points::32-big,
|
|
_optional_octets::8,
|
|
_interpretation::8,
|
|
template::16-big,
|
|
rest::binary
|
|
>> = body
|
|
|
|
grid_params = parse_grid_template(template, rest)
|
|
Map.put(acc, :grid_params, grid_params)
|
|
end
|
|
|
|
defp parse_section(4, body, acc) do
|
|
<<
|
|
_num_coords::16-big,
|
|
_template::16-big,
|
|
param_category::8,
|
|
param_number::8,
|
|
_generating_process::8,
|
|
_background::8,
|
|
_forecast_process::8,
|
|
_cutoff_hours::16-big,
|
|
_cutoff_minutes::8,
|
|
_time_unit::8,
|
|
_forecast_time::32-big,
|
|
first_surface_type::8,
|
|
_first_scale_factor::8,
|
|
first_scaled_value::32-big,
|
|
_rest::binary
|
|
>> = body
|
|
|
|
discipline = Map.get(acc, :discipline, 0)
|
|
var = identify_var(discipline, param_category, param_number)
|
|
level = identify_level(first_surface_type, first_scaled_value)
|
|
|
|
Map.put(acc, :product, %{var: var, level: level})
|
|
end
|
|
|
|
defp parse_section(5, body, acc) do
|
|
<<
|
|
num_data_points::32-big,
|
|
template::16-big,
|
|
ref_value_bytes::binary-size(4),
|
|
binary_scale_bytes::binary-size(2),
|
|
decimal_scale_bytes::binary-size(2),
|
|
bits_per_value::8,
|
|
rest::binary
|
|
>> = body
|
|
|
|
<<ref_value::float-32-big>> = ref_value_bytes
|
|
|
|
base_params = %{
|
|
num_data_points: num_data_points,
|
|
template: template,
|
|
reference_value: ref_value,
|
|
binary_scale: sign_magnitude_16(binary_scale_bytes),
|
|
decimal_scale: sign_magnitude_16(decimal_scale_bytes),
|
|
bits_per_value: bits_per_value
|
|
}
|
|
|
|
packing_params =
|
|
case template do
|
|
0 -> base_params
|
|
3 -> parse_complex_packing_params(base_params, rest)
|
|
_ -> base_params
|
|
end
|
|
|
|
Map.put(acc, :packing_params, packing_params)
|
|
end
|
|
|
|
defp parse_section(6, <<indicator::8, _rest::binary>>, acc) do
|
|
bitmap =
|
|
if indicator == 255 do
|
|
:none
|
|
else
|
|
:present
|
|
end
|
|
|
|
Map.put(acc, :bitmap, bitmap)
|
|
end
|
|
|
|
defp parse_section(7, data, acc) do
|
|
Map.put(acc, :data, data)
|
|
end
|
|
|
|
defp parse_section(_section_num, _body, acc) do
|
|
# Skip unknown sections (Section 2, etc.)
|
|
acc
|
|
end
|
|
|
|
defp parse_complex_packing_params(base, rest) do
|
|
<<
|
|
_original_type::8,
|
|
_splitting_method::8,
|
|
missing_mgmt::8,
|
|
_primary_missing::32-big,
|
|
_secondary_missing::32-big,
|
|
num_groups::32-big,
|
|
ref_group_widths::8,
|
|
nbits_group_widths::8,
|
|
ref_group_lengths::32-big,
|
|
length_increment::8,
|
|
last_group_length::32-big,
|
|
nbits_group_lengths::8,
|
|
spatial_order::8,
|
|
num_extra_octets::8,
|
|
_extra::binary
|
|
>> = rest
|
|
|
|
Map.merge(base, %{
|
|
missing_mgmt: missing_mgmt,
|
|
num_groups: num_groups,
|
|
ref_group_widths: ref_group_widths,
|
|
nbits_group_widths: nbits_group_widths,
|
|
ref_group_lengths: ref_group_lengths,
|
|
length_increment: length_increment,
|
|
last_group_length: last_group_length,
|
|
nbits_group_lengths: nbits_group_lengths,
|
|
spatial_order: spatial_order,
|
|
num_extra_octets: num_extra_octets
|
|
})
|
|
end
|
|
|
|
# Template 3.30 — Lambert Conformal Conic
|
|
defp parse_grid_template(30, body) do
|
|
<<
|
|
_shape::8,
|
|
_scale_factor_radius::8,
|
|
_scaled_radius::32-big,
|
|
_scale_factor_major::8,
|
|
_scaled_major::32-big,
|
|
_scale_factor_minor::8,
|
|
_scaled_minor::32-big,
|
|
nx::32-big,
|
|
ny::32-big,
|
|
la1_micro::32-big,
|
|
lo1_micro::32-big,
|
|
_resolution::8,
|
|
lad_micro::32-big,
|
|
lov_micro::32-big,
|
|
dx_mm::32-big,
|
|
dy_mm::32-big,
|
|
_projection_centre::8,
|
|
scan_mode::8,
|
|
latin1_micro::32-big,
|
|
latin2_micro::32-big,
|
|
_lat_sp::32-big,
|
|
_lon_sp::32-big,
|
|
_rest::binary
|
|
>> = body
|
|
|
|
%{
|
|
nx: nx,
|
|
ny: ny,
|
|
la1: la1_micro / 1_000_000,
|
|
lo1: lo1_micro / 1_000_000,
|
|
lad: lad_micro / 1_000_000,
|
|
lov: lov_micro / 1_000_000,
|
|
dx: dx_mm / 1000,
|
|
dy: dy_mm / 1000,
|
|
latin1: latin1_micro / 1_000_000,
|
|
latin2: latin2_micro / 1_000_000,
|
|
scan_mode: scan_mode
|
|
}
|
|
end
|
|
|
|
defp parse_grid_template(template, _body) do
|
|
%{error: "unsupported grid template #{template}"}
|
|
end
|
|
|
|
defp build_result(acc) do
|
|
required = [:grid_params, :product, :packing_params, :data, :bitmap]
|
|
|
|
if Enum.all?(required, &Map.has_key?(acc, &1)) do
|
|
{:ok, Map.take(acc, required)}
|
|
else
|
|
missing = Enum.reject(required, &Map.has_key?(acc, &1))
|
|
{:error, "missing sections: #{inspect(missing)}"}
|
|
end
|
|
end
|
|
end
|