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);:codalso 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, :obs (station observation
data, RINEX 3 / CRINEX). 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:
- Resolve the canonical filename and cache path (pure, from the catalog).
- If the file is already cached, verify it: against the caller's
:sha256when 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. - Otherwise (and only when not
offline:) download the.gzover 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.jsonsidecar (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— whentrue, never touch the network (default from app config, elsefalse):cache_dir— cache root (default:filename.basedir(:user_cache, "orbis/gnss"), overridable viaconfig :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: trueand 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 butReqis 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
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.
Fetch a station observation product and load it into an Orbis.RinexObs
handle.
Build a Product for any center/content/date/sample, returning a tuple.
Fetch an SP3 product and load it into an Orbis.SP3 handle.
Build a daily station observation product (RINEX 3 CRINEX, 30 s default).
Types
@type error() :: {:error, term()}
A fetch error, always a tagged tuple. See the module docs.
Functions
@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.
@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, _}.
@spec mgex_clk(atom(), Date.t(), keyword()) :: Orbis.GnssData.Product.t()
Build an MGEX clock (RINEX clock) product. Defaults to 30S sampling.
@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"}
@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"}
@spec observations( Orbis.GnssData.Product.t(), keyword() ) :: {:ok, Orbis.RinexObs.t()} | error()
Fetch a station observation product and load it into an Orbis.RinexObs
handle.
fetch/2 gunzips the .gz; the committed cache file is the (still Hatanaka)
CRINEX text, which Orbis.RinexObs.load/1 decodes before parsing. Returns
{:ok, %Orbis.RinexObs{}} or a typed error.
@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.
@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.
@spec station_obs(String.t(), Date.t(), keyword()) :: Orbis.GnssData.Product.t()
Build a daily station observation product (RINEX 3 CRINEX, 30 s default).
Station observation files are keyed by a 9-character site id (e.g.
"ESBC00DNK"), not an analysis-center token, and resolve on the ESA GSSC
anonymous archive's daily data tree. Override the sampling with sample:.
Examples
iex> p = Orbis.GnssData.station_obs("ESBC00DNK", ~D[2020-06-25])
iex> Orbis.GnssData.Product.canonical_filename(p)
{:ok, "ESBC00DNK_R_20201770000_01D_30S_MO.crx"}