Current section
Files
Jump to
Current section
Files
lib/sidereon/gnss/constellation.ex
defmodule Sidereon.GNSS.Constellation do
@moduledoc """
GNSS constellation identity catalogs and validation helpers.
This module is a data/catalog layer: it builds normalized satellite identity
records from public sources and compares those records with GNSS products. It
does not alter positioning solves or infer application-specific health rules.
The identity, parsing, validation, and diff logic lives in the `sidereon_core`
`constellation` module (the shared Rust core), reached through the NIF. The
core covers all five GNSS constellations — **GPS, Galileo, GLONASS, BeiDou, and
QZSS** — so this module is multi-system: `from_celestrak_omm/2` dispatches on
the constellation atom (`:gps`, `:galileo`, `:glonass`, `:beidou`, `:qzss`) to
the per-system identity adapter in the core.
OMM/JSON source records are parsed from `OBJECT_NAME` and rendered as the
SP3/RINEX id (`"G13"`, `"E07"`, `"R13"`, `"C19"`, `"J02"`). GPS constellation
status HTML can be parsed and merged as an optional SVN/usability overlay.
## Examples
iex> omms = [
...> %{"OBJECT_NAME" => "GPS BIIF-8 (PRN 03)", "NORAD_CAT_ID" => 40294}
...> ]
iex> {:ok, [record]} = Sidereon.GNSS.Constellation.from_celestrak_omm(:gps, omms)
iex> {record.system, record.prn, record.norad_id, record.sp3_id}
{:gps, 3, 40294, "G03"}
iex> record = %Sidereon.GNSS.Constellation.Record{
...> system: :gps,
...> prn: 3,
...> svn: nil,
...> norad_id: 40294,
...> sp3_id: "G03",
...> active?: true,
...> usable?: true,
...> source: %{}
...> }
iex> Sidereon.GNSS.Constellation.to_csv([record])
"prn,norad_cat_id,active,sp3_id\\n3,40294,true,G03\\n"
Use `from_celestrak_omm/2`, `from_celestrak_omm_lenient/2`,
`parse_navcen_html/1`, and `merge_navcen/2` with caller-provided text or
decoded maps.
"""
alias Sidereon.GNSS.Constellation
alias Sidereon.GNSS.SP3
alias Sidereon.NIF
# The five constellations the catalog resolves identities for. (The core's
# `GnssSystem` also has NavIC/SBAS, but there is no OMM identity adapter for
# them, so they are out of scope for this module.)
@system_letters %{gps: "G", glonass: "R", galileo: "E", beidou: "C", qzss: "J"}
@letter_systems Map.new(@system_letters, fn {atom, letter} -> {letter, atom} end)
# Sort order matching the core's `GnssSystem` enum (used to keep multi-system
# results deterministic), not Elixir's atom ordering.
@system_order %{gps: 0, glonass: 1, galileo: 2, beidou: 3, qzss: 4}
@type system :: :gps | :glonass | :galileo | :beidou | :qzss
defmodule Record do
@moduledoc """
A normalized GNSS satellite identity record.
`active?` means the satellite is present in the base source used to build the
catalog. `usable?` is an advisory health/status flag; for GPS it is `true`
unless a merged NAVCEN row has an active NANU that marks the PRN unusable or
decommissioned. The `source` map preserves source-specific provenance under
`:celestrak`, `:navcen`, and `:navcen_conflict`.
`fdma_channel` is the GLONASS FDMA L1/L2 frequency-channel number `k`
(`-7..=6`); it is `nil` for the CDMA constellations (GPS, Galileo, BeiDou,
QZSS). It is the one identity datum not present in any OMM feed; the core
resolves it from the orbital slot via the published IGS/MCC slot-channel
table (`glonass_fdma_channel/1`).
"""
@enforce_keys [:system, :prn, :norad_id, :sp3_id, :active?, :usable?, :source]
defstruct [
:system,
:prn,
:svn,
:norad_id,
:sp3_id,
:fdma_channel,
:active?,
:usable?,
:source
]
@type t :: %__MODULE__{
system: Constellation.system(),
prn: pos_integer(),
svn: pos_integer() | nil,
norad_id: pos_integer(),
sp3_id: String.t(),
fdma_channel: integer() | nil,
active?: boolean(),
usable?: boolean(),
source: map()
}
end
defmodule SkippedOmm do
@moduledoc """
An OMM entry that `Sidereon.GNSS.Constellation.from_celestrak_omm_lenient/2`
could not resolve to a `Record` for the requested system.
Carries the entry's identity so the caller can triage why it was skipped: a
record from another constellation in a combined feed, or a satellite of the
requested system whose `OBJECT_NAME` does not yet resolve.
"""
@enforce_keys [:object_name, :norad_id]
defstruct [:object_name, :norad_id]
@type t :: %__MODULE__{
object_name: String.t() | nil,
norad_id: pos_integer()
}
end
defmodule Catalog do
@moduledoc """
The result of a lenient catalog build: the records that resolved, plus the
OMM entries that did not.
Resolvable entries become `records` (sorted by `{system, prn}`); every entry
whose `OBJECT_NAME` did not resolve is collected into `skipped` with its
identity, in input order.
"""
alias Sidereon.GNSS.Constellation.SkippedOmm
@enforce_keys [:records, :skipped]
defstruct [:records, :skipped]
@type t :: %__MODULE__{
records: [Sidereon.GNSS.Constellation.Record.t()],
skipped: [SkippedOmm.t()]
}
end
defmodule NavcenStatus do
@moduledoc """
A parsed row from NAVCEN's GPS constellation status table.
"""
@enforce_keys [:system, :prn, :svn, :usable?, :active_nanu?]
defstruct [
:system,
:prn,
:svn,
:usable?,
:active_nanu?,
:nanu_type,
:nanu_subject,
:plane,
:slot,
:block_type,
:clock
]
@type t :: %__MODULE__{
system: Constellation.system(),
prn: pos_integer(),
svn: pos_integer() | nil,
usable?: boolean(),
active_nanu?: boolean(),
nanu_type: String.t() | nil,
nanu_subject: String.t() | nil,
plane: String.t() | nil,
slot: String.t() | nil,
block_type: String.t() | nil,
clock: String.t() | nil
}
end
defmodule Validation do
@moduledoc """
Validation report for a GNSS constellation catalog.
`duplicate_prns` and `inactive_unusable_prns` are keyed by `{system, prn}` so
a legitimate multi-system catalog (GPS PRN 1 and Galileo PRN 1) is not a
false duplicate.
"""
@enforce_keys [
:missing_sp3_ids,
:duplicate_prns,
:duplicate_norad_ids,
:inactive_unusable_prns,
:extra_sp3_ids
]
defstruct [
:missing_sp3_ids,
:duplicate_prns,
:duplicate_norad_ids,
:inactive_unusable_prns,
:extra_sp3_ids
]
@type prn_key :: {Constellation.system(), pos_integer()}
@type t :: %__MODULE__{
missing_sp3_ids: [String.t()],
duplicate_prns: [prn_key()],
duplicate_norad_ids: [pos_integer()],
inactive_unusable_prns: [prn_key()],
extra_sp3_ids: [String.t()]
}
end
defmodule Diff do
@moduledoc """
Change report between two GNSS constellation catalog snapshots.
"""
@enforce_keys [
:added,
:removed,
:norad_reassigned,
:sp3_id_changed,
:svn_changed,
:fdma_channel_changed,
:activity_changed,
:usability_changed
]
defstruct [
:added,
:removed,
:norad_reassigned,
:sp3_id_changed,
:svn_changed,
:fdma_channel_changed,
:activity_changed,
:usability_changed
]
@type record_change(value) :: %{
required(:system) => Constellation.system(),
required(:prn) => pos_integer(),
required(:from) => value,
required(:to) => value
}
@type t :: %__MODULE__{
added: [Record.t()],
removed: [Record.t()],
norad_reassigned: [record_change(pos_integer())],
sp3_id_changed: [record_change(String.t())],
svn_changed: [record_change(pos_integer() | nil)],
fdma_channel_changed: [record_change(integer() | nil)],
activity_changed: [record_change(boolean())],
usability_changed: [record_change(boolean())]
}
end
@type error ::
{:error, {:unsupported_system, term()}}
| {:error, {:bad_celestrak_record, term(), map()}}
| {:error, {:invalid_input, term()}}
| {:error, {:bad_navcen_html, term()}}
| {:error, term()}
@doc """
Build records for `system` from a JSON OMM feed.
This is the string-input sibling of `from_celestrak_omm/2`; decoded entries
are still resolved by the shared Rust core.
"""
@spec from_celestrak_json(String.t(), system()) :: {:ok, [Record.t()]} | error()
def from_celestrak_json(json, system \\ :gps)
def from_celestrak_json(json, system) when is_binary(json) do
with {:ok, letter} <- system_letter(system) do
case NIF.constellation_from_celestrak_json(letter, json) do
{:ok, records} ->
{:ok, Enum.map(records, &from_nif_record/1)}
{:error, {:missing_prn, name}} ->
{:error, {:bad_celestrak_record, {:missing_prn, name}, nil}}
{:error, reason} ->
{:error, reason}
end
end
end
def from_celestrak_json(other, _system), do: {:error, {:invalid_input, {:not_a_binary, other}}}
@doc """
Build records for `system` from CelesTrak OMM/JSON maps, failing on the first
entry whose `OBJECT_NAME` does not resolve to a PRN for `system`.
CelesTrak does not publish SVN, so records built from this source alone have
`svn: nil`. Records are returned sorted by `{system, prn}`.
"""
@spec from_celestrak_omm(system(), [map()]) :: {:ok, [Record.t()]} | error()
def from_celestrak_omm(system, omms) when is_list(omms) do
with {:ok, letter} <- system_letter(system),
{:ok, lites} <- omm_lites(omms) do
case NIF.constellation_from_celestrak_omm(letter, lites) do
{:ok, records} ->
{:ok, Enum.map(records, &from_nif_record/1)}
{:error, {:missing_prn, name}} ->
{:error, {:bad_celestrak_record, {:missing_prn, name}, find_omm(omms, name)}}
{:error, reason} ->
{:error, reason}
end
end
end
def from_celestrak_omm(_system, other), do: {:error, {:invalid_input, {:not_a_list, other}}}
@doc """
Build a lenient catalog from a JSON OMM feed.
This is the string-input sibling of `from_celestrak_omm_lenient/2`.
"""
@spec from_celestrak_json_lenient(String.t(), system()) :: {:ok, Catalog.t()} | error()
def from_celestrak_json_lenient(json, system \\ :gps)
def from_celestrak_json_lenient(json, system) when is_binary(json) do
with {:ok, letter} <- system_letter(system) do
case NIF.constellation_from_celestrak_json_lenient(letter, json) do
{:ok, %{records: records, skipped: skipped}} ->
{:ok,
%Catalog{
records: Enum.map(records, &from_nif_record/1),
skipped: Enum.map(skipped, &from_nif_skipped/1)
}}
{:error, reason} ->
{:error, reason}
end
end
end
def from_celestrak_json_lenient(other, _system), do: {:error, {:invalid_input, {:not_a_binary, other}}}
@doc """
Build records for `system` from CelesTrak OMM/JSON maps, skipping (rather than
aborting on) entries whose `OBJECT_NAME` does not resolve to a PRN for
`system`.
The lenient sibling of `from_celestrak_omm/2`: every OMM that resolves becomes
a `Record`; every entry that does not (an other-constellation satellite in a
combined `gnss` feed, or a freshly launched vehicle whose name does not yet
resolve) is collected into `Catalog.skipped` with its identity. Resolvable
records are returned sorted by `{system, prn}`.
Leniency covers identity resolution only: an entry missing a valid
`NORAD_CAT_ID` still aborts with `{:error, reason}`.
## Examples
iex> omms = [
...> %{"OBJECT_NAME" => "GPS BIIF-8 (PRN 03)", "NORAD_CAT_ID" => 40294},
...> %{"OBJECT_NAME" => "GSAT0210 (GALILEO 13)", "NORAD_CAT_ID" => 41859}
...> ]
iex> {:ok, catalog} = Sidereon.GNSS.Constellation.from_celestrak_omm_lenient(:gps, omms)
iex> Enum.map(catalog.records, & &1.sp3_id)
["G03"]
iex> Enum.map(catalog.skipped, &{&1.object_name, &1.norad_id})
[{"GSAT0210 (GALILEO 13)", 41859}]
"""
@spec from_celestrak_omm_lenient(String.t()) :: {:ok, Catalog.t()} | error()
def from_celestrak_omm_lenient(json) when is_binary(json) do
from_celestrak_omm_lenient(json, :gps)
end
@spec from_celestrak_omm_lenient(String.t(), system()) :: {:ok, Catalog.t()} | error()
def from_celestrak_omm_lenient(json, system) when is_binary(json) do
from_celestrak_json_lenient(json, system)
end
@spec from_celestrak_omm_lenient(system(), [map()]) :: {:ok, Catalog.t()} | error()
def from_celestrak_omm_lenient(system, omms) when is_list(omms) do
with {:ok, letter} <- system_letter(system),
{:ok, lites} <- omm_lites(omms) do
case NIF.constellation_from_celestrak_omm_lenient(letter, lites) do
{:ok, %{records: records, skipped: skipped}} ->
{:ok,
%Catalog{
records: Enum.map(records, &from_nif_record/1),
skipped: Enum.map(skipped, &from_nif_skipped/1)
}}
{:error, reason} ->
{:error, reason}
end
end
end
def from_celestrak_omm_lenient(_system, other), do: {:error, {:invalid_input, {:not_a_list, other}}}
@doc """
Parse NAVCEN's GPS constellation status HTML.
Returns status rows keyed by PRN/SVN; merge them into CelesTrak records with
`merge_navcen/2`.
"""
@spec parse_navcen_html(String.t()) :: {:ok, [NavcenStatus.t()]} | error()
def parse_navcen_html(html) when is_binary(html) do
case NIF.constellation_parse_navcen(html) do
{:ok, statuses} -> {:ok, Enum.map(statuses, &from_nif_status/1)}
{:error, reason} -> {:error, {:bad_navcen_html, reason}}
end
end
def parse_navcen_html(_), do: {:error, {:bad_navcen_html, :not_binary}}
@doc """
Merge NAVCEN status rows into normalized records by PRN.
NAVCEN does not publish NORAD ids, so CelesTrak stays the identity base. When a
PRN is present in both sources and the block types are compatible, this fills
`svn`, updates `usable?`, and records the NAVCEN provenance under
`source.navcen`. A NAVCEN row that matches the PRN but carries an incompatible
block type (a PRN transition) is recorded under `source.navcen_conflict`.
"""
@spec merge_navcen([Record.t()], [NavcenStatus.t()]) :: [Record.t()]
def merge_navcen(records, statuses) when is_list(records) and is_list(statuses) do
# The core merges by PRN only, so a NAVCEN row for PRN 3 would otherwise
# overlay onto any constellation's PRN 3. NAVCEN is a GPS oracle; merge each
# system's records only against that same system's status rows.
statuses_by_system = Enum.group_by(statuses, & &1.system)
records
|> Enum.group_by(& &1.system)
|> Enum.flat_map(fn {system, system_records} ->
case Map.fetch(statuses_by_system, system) do
{:ok, system_statuses} -> nif_merge_navcen(system_records, system_statuses)
:error -> system_records
end
end)
|> Enum.sort_by(&{Map.fetch!(@system_order, &1.system), &1.prn})
end
defp nif_merge_navcen(records, statuses) do
NIF.constellation_merge_navcen(
Enum.map(records, &to_nif_record/1),
Enum.map(statuses, &to_nif_status/1)
)
|> Enum.map(&from_nif_record/1)
end
@doc """
Export records as the compact mapping CSV:
prn,norad_cat_id,active,sp3_id
The `active` column is `true` only when `record.active?` and `record.usable?`
are both true. Records are sorted by `{system, prn}`.
## Options
* `:booleans` - how the `active` column is rendered: `:lower` (default:
`true`/`false`) or `:title` (`True`/`False`, for a pandas consumer).
"""
@spec to_csv([Record.t()], keyword()) :: String.t()
def to_csv(records, opts \\ []) when is_list(records) do
booleans = if Keyword.get(opts, :booleans, :lower) == :title, do: "title", else: "lower"
NIF.constellation_to_csv(Enum.map(records, &to_nif_record/1), booleans)
end
@doc """
Compare two constellation catalog snapshots by `{system, prn}` identity.
The result separates added/removed PRNs from identity/status changes on a PRN
that exists in both snapshots. Assumes each input has at most one record per
`{system, prn}`; run `validate/1` first on hand-edited catalogs.
iex> previous = [
...> %Sidereon.GNSS.Constellation.Record{
...> system: :gps, prn: 3, svn: 69, norad_id: 40294, sp3_id: "G03",
...> active?: true, usable?: true, source: %{}
...> }
...> ]
iex> current = [%{hd(previous) | norad_id: 99999, active?: false}]
iex> diff = Sidereon.GNSS.Constellation.diff(previous, current)
iex> [norad] = diff.norad_reassigned
iex> {norad.system, norad.prn, norad.from, norad.to}
{:gps, 3, 40294, 99999}
iex> [activity] = diff.activity_changed
iex> {activity.system, activity.prn, activity.from, activity.to}
{:gps, 3, true, false}
iex> Sidereon.GNSS.Constellation.changed?(diff)
true
"""
@spec diff([Record.t()], [Record.t()]) :: Diff.t()
def diff(previous, current) when is_list(previous) and is_list(current) do
NIF.constellation_diff(
Enum.map(previous, &to_nif_record/1),
Enum.map(current, &to_nif_record/1)
)
|> from_nif_diff()
end
def diff(_previous, _current) do
raise ArgumentError, "Sidereon.GNSS.Constellation.diff/2 expects two record lists"
end
@doc """
Validate catalog identity without an SP3 product.
Reports duplicate PRNs, duplicate NORAD ids, and PRNs that are inactive or
unusable according to the normalized records.
"""
@spec validate([Record.t()]) :: Validation.t()
def validate(records) when is_list(records) do
records
|> Enum.map(&to_nif_record/1)
|> NIF.constellation_validate()
|> from_nif_validation()
end
@doc """
Validate catalog identity against SP3 satellite ids.
The second argument may be a loaded `%Sidereon.GNSS.SP3{}` or a list of SP3/RINEX
satellite tokens. `missing_sp3_ids` reports active+usable catalog ids absent
from the product; `extra_sp3_ids` reports product ids absent from the
active+usable catalog, restricted to the systems the catalog covers.
"""
@spec validate_sp3([Record.t()], SP3.t() | [String.t()]) :: Validation.t()
def validate_sp3(records, %SP3{} = sp3), do: validate_sp3(records, SP3.satellite_ids(sp3))
def validate_sp3(records, sp3_ids) when is_list(records) and is_list(sp3_ids) do
records
|> Enum.map(&to_nif_record/1)
|> NIF.constellation_validate_against_sp3_ids(sp3_ids)
|> from_nif_validation()
end
@doc """
Validate against SP3 satellite ids and raise unless the catalog is clean.
A build-time gate: returns `:ok` when the catalog has no findings, otherwise
raises `ArgumentError` describing them. Intended for catalog-build / automation
steps, not the positioning runtime.
"""
@spec validate_sp3!([Record.t()], SP3.t() | [String.t()]) :: :ok
def validate_sp3!(records, %SP3{} = sp3), do: validate_sp3!(records, SP3.satellite_ids(sp3))
def validate_sp3!(records, sp3_ids) when is_list(records) and is_list(sp3_ids) do
case NIF.constellation_validate_against_sp3_ids_strict(
Enum.map(records, &to_nif_record/1),
sp3_ids
) do
:ok -> :ok
{:error, message} -> raise ArgumentError, "GNSS catalog failed SP3 validation: " <> message
end
end
@doc """
Returns `true` when a validation report has no findings.
"""
@spec valid?(Validation.t()) :: boolean()
def valid?(%Validation{} = report) do
report.missing_sp3_ids == [] and report.duplicate_prns == [] and
report.duplicate_norad_ids == [] and report.inactive_unusable_prns == [] and
report.extra_sp3_ids == []
end
@doc """
Returns `true` when a constellation diff has any findings.
"""
@spec changed?(Diff.t()) :: boolean()
def changed?(%Diff{} = diff) do
diff.added != [] or diff.removed != [] or diff.norad_reassigned != [] or
diff.sp3_id_changed != [] or diff.svn_changed != [] or diff.fdma_channel_changed != [] or
diff.activity_changed != [] or diff.usability_changed != []
end
@doc """
Render the canonical SP3/RINEX satellite token for a supported GNSS PRN.
iex> Sidereon.GNSS.Constellation.sp3_id(:gps, 7)
"G07"
iex> Sidereon.GNSS.Constellation.sp3_id(:galileo, 7)
"E07"
"""
@spec sp3_id(system(), pos_integer()) :: String.t()
def sp3_id(system, prn) when is_integer(prn) and prn > 0 do
NIF.constellation_sp3_id(Map.fetch!(@system_letters, system), prn)
end
@doc """
Resolve a GLONASS orbital slot (`1..24`) to its FDMA L1/L2 frequency-channel
number `k`, or `nil` for a slot outside the operational range.
iex> Sidereon.GNSS.Constellation.glonass_fdma_channel(1)
1
iex> Sidereon.GNSS.Constellation.glonass_fdma_channel(2)
-4
iex> Sidereon.GNSS.Constellation.glonass_fdma_channel(0)
nil
"""
@spec glonass_fdma_channel(integer()) :: integer() | nil
def glonass_fdma_channel(slot) when is_integer(slot), do: NIF.constellation_glonass_fdma_channel(slot)
@doc """
Resolve a Galileo `GSATdddd` build id to its SVID/PRN (E-number), or `nil` when
no SVID is assigned yet.
iex> Sidereon.GNSS.Constellation.galileo_prn_for_gsat(210)
1
"""
@spec galileo_prn_for_gsat(integer()) :: integer() | nil
def galileo_prn_for_gsat(gsat) when is_integer(gsat), do: NIF.constellation_galileo_prn_for_gsat(gsat)
@doc """
Resolve a GLONASS (Uragan) number to its orbital slot (`1..24`), or `nil` when
the number is not in the published constellation table.
iex> Sidereon.GNSS.Constellation.glonass_slot_for_number(730)
1
"""
@spec glonass_slot_for_number(integer()) :: integer() | nil
def glonass_slot_for_number(number) when is_integer(number), do: NIF.constellation_glonass_slot_for_number(number)
# --- system identity helpers ---------------------------------------------
defp system_letter(system) do
case Map.fetch(@system_letters, system) do
{:ok, letter} -> {:ok, letter}
:error -> {:error, {:unsupported_system, system}}
end
end
defp letter_to_system(letter), do: Map.fetch!(@letter_systems, letter)
# --- OMM extraction (user maps -> NIF OMM-lite maps) ---------------------
defp omm_lites(omms) do
omms
|> Enum.reduce_while({:ok, []}, fn omm, {:ok, acc} ->
case omm_lite(omm) do
{:ok, lite} -> {:cont, {:ok, [lite | acc]}}
{:error, _} = err -> {:halt, err}
end
end)
|> case do
{:ok, lites} -> {:ok, Enum.reverse(lites)}
{:error, _} = err -> err
end
end
defp omm_lite(%{} = omm) do
case fetch_norad(omm) do
{:ok, norad} ->
{:ok,
%{
object_name: optional_string(omm["OBJECT_NAME"]),
norad_id: norad,
object_id: optional_string(omm["OBJECT_ID"]),
epoch: optional_string(omm["EPOCH"])
}}
{:error, reason} ->
{:error, {:bad_celestrak_record, reason, omm}}
end
end
defp omm_lite(other), do: {:error, {:bad_celestrak_record, :not_a_map, %{value: other}}}
defp fetch_norad(omm) do
case Map.fetch(omm, "NORAD_CAT_ID") do
{:ok, value} -> parse_positive_int(value)
:error -> {:error, {:missing_field, "NORAD_CAT_ID"}}
end
end
defp parse_positive_int(value) when is_integer(value) and value > 0, do: {:ok, value}
defp parse_positive_int(value) when is_binary(value) do
case Integer.parse(String.trim(value)) do
{int, ""} when int > 0 -> {:ok, int}
_ -> {:error, {:bad_integer, "NORAD_CAT_ID", value}}
end
end
defp parse_positive_int(value), do: {:error, {:bad_integer, "NORAD_CAT_ID", value}}
defp optional_string(value) when is_binary(value), do: value
defp optional_string(_), do: nil
# Recover the offending OMM map by object name for the error payload, so a
# missing-PRN failure carries the same `{tag, reason, omm}` shape as the other
# bad-record failures.
defp find_omm(omms, name) do
Enum.find(omms, %{}, fn
%{"OBJECT_NAME" => object_name} -> object_name == name
_ -> false
end)
end
# --- NIF record/struct marshaling ----------------------------------------
defp to_nif_record(%Record{} = record) do
%{
system: Map.fetch!(@system_letters, record.system),
prn: record.prn,
svn: record.svn,
norad_id: record.norad_id,
sp3_id: record.sp3_id,
fdma_channel: record.fdma_channel,
active: record.active?,
usable: record.usable?,
source: record.source || %{}
}
end
defp from_nif_record(%{} = map) do
%Record{
system: letter_to_system(map.system),
prn: map.prn,
svn: map.svn,
norad_id: map.norad_id,
sp3_id: map.sp3_id,
fdma_channel: map.fdma_channel,
active?: map.active,
usable?: map.usable,
source: map.source
}
end
defp from_nif_skipped(%{} = map) do
%SkippedOmm{object_name: map.object_name, norad_id: map.norad_id}
end
defp to_nif_status(%NavcenStatus{} = status) do
%{
system: Map.fetch!(@system_letters, status.system),
prn: status.prn,
svn: status.svn,
usable: status.usable?,
active_nanu: status.active_nanu?,
nanu_type: status.nanu_type,
nanu_subject: status.nanu_subject,
plane: status.plane,
slot: status.slot,
block_type: status.block_type,
clock: status.clock
}
end
defp from_nif_status(%{} = map) do
%NavcenStatus{
system: letter_to_system(map.system),
prn: map.prn,
svn: map.svn,
usable?: map.usable,
active_nanu?: map.active_nanu,
nanu_type: map.nanu_type,
nanu_subject: map.nanu_subject,
plane: map.plane,
slot: map.slot,
block_type: map.block_type,
clock: map.clock
}
end
defp from_nif_validation(%{} = map) do
%Validation{
missing_sp3_ids: map.missing_sp3_ids,
duplicate_prns: Enum.map(map.duplicate_prns, &prn_key/1),
duplicate_norad_ids: map.duplicate_norad_ids,
inactive_unusable_prns: Enum.map(map.inactive_unusable_prns, &prn_key/1),
extra_sp3_ids: map.extra_sp3_ids
}
end
defp prn_key({letter, prn}), do: {letter_to_system(letter), prn}
defp from_nif_diff(%{} = map) do
%Diff{
added: Enum.map(map.added, &from_nif_record/1),
removed: Enum.map(map.removed, &from_nif_record/1),
norad_reassigned: Enum.map(map.norad_reassigned, &from_nif_change/1),
sp3_id_changed: Enum.map(map.sp3_id_changed, &from_nif_change/1),
svn_changed: Enum.map(map.svn_changed, &from_nif_change/1),
fdma_channel_changed: Enum.map(map.fdma_channel_changed, &from_nif_change/1),
activity_changed: Enum.map(map.activity_changed, &from_nif_change/1),
usability_changed: Enum.map(map.usability_changed, &from_nif_change/1)
}
end
defp from_nif_change(%{system: letter, prn: prn, from: from, to: to}) do
%{system: letter_to_system(letter), prn: prn, from: from, to: to}
end
end