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(<>, acc) when length > 5 do body_length = length - 5 if body_length <= byte_size(rest) do <> = 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_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, <>, 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