Orbis.GnssData (Orbis v0.7.0)

Copy Markdown View Source

Optional fetch-and-cache layer for GNSS products (SP3, RINEX clock, broadcast navigation, IONEX).

Orbis.GnssData downloads, decompresses, checksums, and records provenance for the precise- and broadcast-product files that Orbis.SP3, Orbis.BroadcastEphemeris, and Orbis.PointPositioning consume — and then hands back a local file path (or a loaded handle). It is deliberately one-directional: the numerical layers never call into this module, so a solve never depends on network availability. You fetch once, then point the solver at the cached file.

Quick start

product = Orbis.GnssData.mgex_sp3(:cod, ~D[2020-06-24])

# Download (or reuse cache) and get a decompressed file path:
{:ok, path} = Orbis.GnssData.fetch(product)

# Or fetch and load in one step:
{:ok, sp3} = Orbis.GnssData.sp3(product)
{:ok, state} = Orbis.SP3.position(sp3, "G01", ~N[2020-06-24 00:00:00])

Catalog

Products are identified by analysis center, content type, date, and sampling.

Supported centers and what each publishes:

  • :gfz — GFZ operational rapid SP3/CLK over HTTPS (isdc-data.gfz.de)
  • :cod, :grg, :wum — CODE / CNES-CLS / Wuhan MGEX precise SP3/CLK over anonymous FTP (ESA GSSC mirror, gssc.esa.int); :cod also serves IONEX
  • :igs — the IGS merged broadcast navigation file (:nav) and the combined global ionosphere map (:ionex), over FTP

Content types: :sp3, :clk, :nav, :ionex. Precise products and IONEX follow the IGS long-name convention AAAVPPPTTT_YYYYDDDHHMM_LEN_SMP_CNT.EXT; broadcast navigation uses the no-sampling RINEX long-name BRDC00IGS_R_YYYYDDDHHMM_01D_MN.rnx. See Orbis.GnssData.Catalog.

The fetch pipeline

fetch/2 is cache-first:

  1. Resolve the canonical filename and cache path (pure, from the catalog).
  2. If the file is already cached, verify it: against the caller's :sha256 when given, otherwise against the decompressed SHA-256 recorded in the file's provenance sidecar (every downloaded file has one). A verified hit returns with no network. A corrupt hit (checksum mismatch) or an unverifiable one (no sidecar — e.g. a hand-placed file) is, online, discarded and re-downloaded; offline, a corrupt hit is terminal and an unverifiable one is returned as the best available.
  3. Otherwise (and only when not offline:) download the .gz over HTTPS (Req) or FTP (:ftp) to memory, decompress with a gzip-bomb cap, verify any known checksum, and atomically commit the decompressed file into the cache (temp file + rename) together with its required .provenance.json sidecar (the commit fails if the sidecar cannot be written, so a cached file always carries its integrity hash).

Offline mode

Pass offline: true (or set config :orbis, gnss_data_offline: true) to forbid all network access: a verified cache hit is returned, a corrupt hit yields {:error, {:checksum_mismatch, _, _}}, and a miss returns {:error, {:offline_miss, name}}. This is how the test suite — and any user without connectivity — runs deterministically.

Network tests

Live-archive fetching is exercised by tests tagged :network, which are excluded by default (including in CI, which has no network); the rest of the suite is fully offline and deterministic. Run the live gate manually with mix test --include network.

Options

  • :offline — when true, never touch the network (default from app config, else false)
  • :cache_dir — cache root (default :filename.basedir(:user_cache, "orbis/gnss"), overridable via config :orbis, gnss_data_cache_dir:)
  • :sha256 — expected SHA-256 (hex) of the decompressed file; verified on both cache hits and fresh downloads
  • :max_decompressed_bytes — gzip-bomb cap (default 500 MiB)
  • :timeout_ms — per-attempt network timeout (default 30_000)
  • :retries — attempts for transient network errors (default 3)
  • :backoff_ms — base backoff between retries, doubled each attempt (default 500)
  • :max_compressed_bytes — cap on the compressed payload buffered into memory while downloading (default 64 MiB)

Typed errors

Every failure is a tagged tuple so callers can branch on it:

  • {:error, {:offline_miss, name}}offline: true and not cached
  • {:error, {:checksum_mismatch, expected, got}} — digest verification failed
  • {:error, {:unsupported_product, detail}} — unknown center/content/sample, or a host outside the catalog
  • {:error, :req_not_available} — HTTPS needed but Req is not loaded
  • {:error, {:http_status, code}} — non-2xx HTTP response
  • {:error, {:redirect_not_allowed, code}} — a 3xx redirect was refused (redirects are not followed, to keep the SSRF allow-list intact)
  • {:error, {:file_not_found, url}} — 404 / missing on the archive
  • {:error, {:network, detail}} — connection/timeout/DNS failure
  • {:error, {:ftp_error, reason}} — FTP-level failure
  • {:error, {:download_size_exceeded, max, got}} — compressed payload cap hit
  • {:error, {:decompress_failed, reason}} — corrupt gzip
  • {:error, {:decompress_size_exceeded, max, got}} — gzip-bomb cap hit
  • {:error, {:cache_dir_not_writable, reason}} — cannot create/write cache
  • {:error, {:provenance_write_failed, reason}} — the product downloaded but its required provenance sidecar could not be written (the product is rolled back so nothing unverifiable is left in the cache)
  • {:error, {:unsafe_cache_name, name}} — filename failed path-safety checks
  • {:error, {:temp_file_error, reason}} — temp write/rename failure

Summary

Types

A fetch error, always a tagged tuple. See the module docs.

Functions

Fetch a broadcast-navigation product and load it into an Orbis.BroadcastEphemeris handle.

Fetch a product, returning the local path to its decompressed file.

Build an MGEX clock (RINEX clock) product. Defaults to 30S sampling.

Build an IONEX (global ionosphere TEC map) product.

Build a broadcast-navigation (merged multi-GNSS RINEX NAV) product.

Build an MGEX SP3 (precise orbit) product for a center and date.

Build a Product for any center/content/date/sample, returning a tuple.

Fetch an SP3 product and load it into an Orbis.SP3 handle.

Types

error()

@type error() :: {:error, term()}

A fetch error, always a tagged tuple. See the module docs.

Functions

broadcast(product, opts \\ [])

@spec broadcast(
  Orbis.GnssData.Product.t(),
  keyword()
) :: {:ok, Orbis.BroadcastEphemeris.t()} | error()

Fetch a broadcast-navigation product and load it into an Orbis.BroadcastEphemeris handle.

Returns {:ok, %Orbis.BroadcastEphemeris{}} or a typed error.

fetch(product, opts \\ [])

@spec fetch(
  Orbis.GnssData.Product.t(),
  keyword()
) :: {:ok, String.t()} | error()

Fetch a product, returning the local path to its decompressed file.

Cache-first: a verified cache hit returns immediately with no network. See the module docs for the full pipeline, options, and error taxonomy.

Returns {:ok, path} or a typed {:error, _}.

mgex_clk(center, date, opts \\ [])

@spec mgex_clk(atom(), Date.t(), keyword()) :: Orbis.GnssData.Product.t()

Build an MGEX clock (RINEX clock) product. Defaults to 30S sampling.

mgex_ionex(center, date, opts \\ [])

@spec mgex_ionex(atom(), Date.t(), keyword()) :: Orbis.GnssData.Product.t()

Build an IONEX (global ionosphere TEC map) product.

Served by :igs (IGS0OPSFIN) and :cod (COD0OPSFIN). IONEX maps are sub-daily, so the sampling defaults to 01H; pass sample: to override (e.g. "02H").

Examples

iex> p = Orbis.GnssData.mgex_ionex(:igs, ~D[2024-06-24])
iex> Orbis.GnssData.Product.canonical_filename(p)
{:ok, "IGS0OPSFIN_20241760000_01D_01H_GIM.INX"}

mgex_nav(center, date, opts \\ [])

@spec mgex_nav(atom(), Date.t(), keyword()) :: Orbis.GnssData.Product.t()

Build a broadcast-navigation (merged multi-GNSS RINEX NAV) product.

Only :igs publishes this product (BRDC00IGS_R_..._MN.rnx). The RINEX navigation long-name carries no sampling field, so the sample argument is not part of the filename; it defaults to 01D purely to satisfy validation.

Examples

iex> p = Orbis.GnssData.mgex_nav(:igs, ~D[2020-06-25])
iex> Orbis.GnssData.Product.canonical_filename(p)
{:ok, "BRDC00IGS_R_20201770000_01D_MN.rnx"}

mgex_sp3(center, date, opts \\ [])

@spec mgex_sp3(atom(), Date.t(), keyword()) :: Orbis.GnssData.Product.t()

Build an MGEX SP3 (precise orbit) product for a center and date.

Defaults to 05M (5-minute) sampling; override with sample:.

Examples

iex> p = Orbis.GnssData.mgex_sp3(:cod, ~D[2020-06-24])
iex> p.center
:cod
iex> Orbis.GnssData.Product.canonical_filename(p)
{:ok, "COD0MGXFIN_20201760000_01D_05M_ORB.SP3"}

product(center, content, date, sample)

@spec product(atom(), atom(), Date.t(), String.t()) ::
  {:ok, Orbis.GnssData.Product.t()} | {:error, {:unsupported_product, term()}}

Build a Product for any center/content/date/sample, returning a tuple.

Use this instead of the bang builders when the inputs may be invalid.

sp3(product, opts \\ [])

@spec sp3(
  Orbis.GnssData.Product.t(),
  keyword()
) :: {:ok, Orbis.SP3.t()} | error()

Fetch an SP3 product and load it into an Orbis.SP3 handle.

Equivalent to fetch/2 followed by Orbis.SP3.load/1. Returns {:ok, %Orbis.SP3{}} or a typed error.