Instead of returning "malformed section" on trailing bytes or truncated sections, attempt to return parsed results. This handles older HRRR files (2019) that have padding bytes after the last section.
265 lines
7.1 KiB
Elixir
265 lines
7.1 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}}`.
|
|
"""
|
|
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.
|
|
"""
|
|
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, 3), do: "PWAT"
|
|
def identify_var(_discipline, cat, num), do: "#{cat}:#{num}"
|
|
|
|
@doc """
|
|
Identify level string from surface type and scaled value.
|
|
"""
|
|
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(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.
|
|
"""
|
|
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
|