defmodule IGC.Headers do @moduledoc """ Represents the known Header attributes of the IGC file. All the attributes, in their raw form, are stored in the `raw_headers` field. """ defstruct [ # Flight recorder id :flight_recorder_id, # Altitude models used :gnss_altitude, :pressure_altitude, # Competition class and id :competition_class, :competition_id, # Date of the flight :date, # Flight number for today :flight_number, # Timezone offset :timezone, # Manufacturer and model of the flight recorder :flight_recorder_type, # Glider information :glider_id, :glider_type, # Pilot in command :pilot, # Recorder hardware info :pressure_sensor, :firmware_version, :hardware_version, # All the raw headers as they were parsed raw_headers: %{}, # All the fix record extensions fix_extensions: %IGC.Extensions{}, # All the data record extensions data_extensions: %IGC.Extensions{} ] @type t :: %__MODULE__{ raw_headers: raw_headers(), flight_recorder_id: String.t() | nil, gnss_altitude: maybe_value(), pressure_altitude: maybe_value(), competition_class: maybe_value(), competition_id: maybe_value(), date: Date.t() | nil, flight_number: integer | nil, timezone: maybe_value(), flight_recorder_type: maybe_value(), glider_id: maybe_value(), glider_type: maybe_value(), pilot: maybe_value(), pressure_sensor: maybe_value(), firmware_version: maybe_value(), hardware_version: maybe_value(), fix_extensions: IGC.Extensions.t(), data_extensions: IGC.Extensions.t() } @type raw_headers :: %{String.t() => {String.t() | nil, String.t()}} @type maybe_value :: String.t() | nil | :unknown @doc """ Parses a "H" record line, populating the result in the IGC.Header struct """ @spec parse_line(String.t(), t()) :: {:ok, t()} | {:error, atom} def parse_line(line, headers \\ %__MODULE__{}) do with {:ok, _source, rest} <- IGC.Parser.take(line, 1), {:ok, short_code, rest} <- IGC.Parser.take(rest, 3), {:ok, subject, value} <- parse_subject(rest), {:ok, headers} <- assign_known(headers, short_code, value) do {:ok, %{headers | raw_headers: Map.put(headers.raw_headers, short_code, {subject, value})}} end end @spec parse_subject(String.t(), String.t()) :: {:ok, String.t(), String.t()} | {:error, atom} defp parse_subject(line, subject \\ "") do case String.next_codepoint(line) do {":", rest} -> {:ok, subject, rest} {char, rest} -> parse_subject(rest, subject <> char) # No subject found. Not all entries have a subject. _ -> {:ok, nil, subject} end end @spec assign_known(t(), String.t(), String.t()) :: {:ok, t()} | {:error, atom} defp assign_known(headers, short_code, value) do case short_code do "ALG" -> {:ok, %{headers | gnss_altitude: maybe(value)}} "ALP" -> {:ok, %{headers | pressure_altitude: maybe(value)}} "CCL" -> {:ok, %{headers | competition_class: maybe(value)}} "CID" -> {:ok, %{headers | competition_id: maybe(value)}} "FTY" -> {:ok, %{headers | flight_recorder_type: maybe(value)}} "GID" -> {:ok, %{headers | glider_id: maybe(value)}} "GTY" -> {:ok, %{headers | glider_type: maybe(value)}} "PLT" -> {:ok, %{headers | pilot: maybe(value)}} "PRS" -> {:ok, %{headers | pressure_sensor: maybe(value)}} "RFW" -> {:ok, %{headers | firmware_version: maybe(value)}} "RHW" -> {:ok, %{headers | hardware_version: maybe(value)}} "TZN" -> {:ok, %{headers | timezone: maybe(value)}} "DTE" -> with {:ok, date, flight_number} <- parse_date(value) do {:ok, %{headers | date: date, flight_number: flight_number}} end # Ignore the rest _ -> {:ok, headers} end end @spec parse_date(String.t()) :: {:ok, Date.t(), integer() | nil} | {:error, atom} defp parse_date(value) do with {:ok, day, rest} <- IGC.Parser.take(value, 2, :int), {:ok, month, rest} <- IGC.Parser.take(rest, 2, :int), {:ok, year, rest} <- IGC.Parser.take(rest, 2, :int), {:ok, flight_number} <- parse_flight_number(rest), {:ok, date} <- Date.new(2000 + year, month, day) do {:ok, date, flight_number} end end @spec parse_flight_number(String.t()) :: {:ok, integer() | nil} | {:error, atom} defp parse_flight_number(value) when value == "", do: {:ok, nil} defp parse_flight_number(value) do with {:ok, _comma, rest} <- IGC.Parser.take(value, 1), {:ok, flight_number, _rest} <- IGC.Parser.take(rest, 2, :int) do {:ok, flight_number} end end @spec maybe(String.t()) :: maybe_value() defp maybe(value) when value == "NKN", do: :unknown defp maybe(value), do: value end