Optional fetch-and-cache layer for GNSS products (SP3, RINEX clock, broadcast navigation, IONEX).
Orbis.GNSS.Data downloads, decompresses, checksums, and records provenance
for the precise- and broadcast-product files that Orbis.GNSS.SP3,
Orbis.GNSS.Broadcast, and Orbis.GNSS.Positioning 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.GNSS.Data.mgex_sp3(:cod, ~D[2020-06-24])
# Download (or reuse cache) and get a decompressed file path:
{:ok, path} = Orbis.GNSS.Data.fetch(product)
# Or fetch and load in one step:
{:ok, sp3} = Orbis.GNSS.Data.sp3(product)
{:ok, state} = Orbis.GNSS.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_ult,:cod_ult,:esa_ult,:gfz_ult,:grg_ult— ultra-rapidOPSULTSP3 products over anonymous FTP for current-day/live-latency use (:grg_ultalso serves ultra clocks):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.GNSS.Data.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, a required dependency) 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 downloads are disabled by config{: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.GNSS.Broadcast handle.
Fetch a product, returning the local path to its decompressed file.
Fetch SP3 products from several centers and merge the available ones.
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.GNSS.RINEX.Observations
handle.
Build an ultra-rapid OPS clock product.
Build an ultra-rapid OPS SP3 product.
Build a Product for any center/content/date/sample, returning a tuple.
Build a Product for any center/content/date/sample with product-specific
options such as issue: "0600" for ultra-rapid products.
Fetch an SP3 product and load it into an Orbis.GNSS.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.GNSS.Data.Product.t(), keyword() ) :: {:ok, Orbis.GNSS.Broadcast.t()} | error()
Fetch a broadcast-navigation product and load it into an
Orbis.GNSS.Broadcast handle.
Returns {:ok, %Orbis.GNSS.Broadcast{}} or a typed error.
@spec fetch( Orbis.GNSS.Data.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 fetch_merged_sp3( Date.t() | NaiveDateTime.t() | DateTime.t(), [atom()], keyword() ) :: {:ok, Orbis.GNSS.SP3.t(), map()} | {:error, {:no_products, [map()]}} | {:error, {:incompatible_sources, map()}} | error()
Fetch SP3 products from several centers and merge the available ones.
centers are tried in precedence order. A missing or not-yet-published center
is recorded in the returned report and does not abort the call. For
ultra-rapid centers and timestamp targets, Orbis tries issue candidates newest
to oldest until it finds a cached/downloadable product, so callers near the
publication frontier can fall back from a not-yet-landed latest issue.
Returns:
{:ok, merged, report}when at least one center contributes. With one contributor,mergedis that source SP3 andreport.single_product?istrue.{:error, {:no_products, reasons}}when every center is absent or fails.{:error, {:incompatible_sources, %{centers:, reason:}}}when the fetched centers exist but cannot be combined (their SP3 headers disagree on time scale or coordinate system, which the merge refuses rather than mixing frames).
The report includes :contributors, :absent, :source_count,
:single_product?, and the normal SP3 merge audit keys (:quarantined,
:single_source, :position_outliers).
Examples
iex> cache = System.tmp_dir!()
iex> {:error, {:no_products, reasons}} =
...> Orbis.GNSS.Data.fetch_merged_sp3(~D[2024-09-03], [:igs_ult], offline: true, cache_dir: cache)
iex> [%{center: :igs_ult, reason: :offline_miss}] = reasons
@spec mgex_clk(atom(), Date.t(), keyword()) :: Orbis.GNSS.Data.Product.t()
Build an MGEX clock (RINEX clock) product. Defaults to 30S sampling.
@spec mgex_ionex(atom(), Date.t(), keyword()) :: Orbis.GNSS.Data.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.GNSS.Data.mgex_ionex(:igs, ~D[2024-06-24])
iex> Orbis.GNSS.Data.Product.canonical_filename(p)
{:ok, "IGS0OPSFIN_20241760000_01D_01H_GIM.INX"}
@spec mgex_sp3(atom(), Date.t(), keyword()) :: Orbis.GNSS.Data.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.GNSS.Data.mgex_sp3(:cod, ~D[2020-06-24])
iex> p.center
:cod
iex> Orbis.GNSS.Data.Product.canonical_filename(p)
{:ok, "COD0MGXFIN_20201760000_01D_05M_ORB.SP3"}
@spec observations( Orbis.GNSS.Data.Product.t(), keyword() ) :: {:ok, Orbis.GNSS.RINEX.Observations.t()} | error()
Fetch a station observation product and load it into an Orbis.GNSS.RINEX.Observations
handle.
fetch/2 gunzips the .gz; the committed cache file is the (still Hatanaka)
CRINEX text, which Orbis.GNSS.RINEX.Observations.load/1 decodes before parsing. Returns
{:ok, %Orbis.GNSS.RINEX.Observations{}} or a typed error.
@spec ops_ultra_clk(atom(), Date.t() | NaiveDateTime.t() | DateTime.t(), keyword()) :: Orbis.GNSS.Data.Product.t()
Build an ultra-rapid OPS clock product.
The anonymous GSSC ultra clock line currently covered by the catalog is
:grg_ult (GRG0OPSULT). Issue-time behavior matches ops_ultra_sp3/3.
@spec ops_ultra_sp3(atom(), Date.t() | NaiveDateTime.t() | DateTime.t(), keyword()) :: Orbis.GNSS.Data.Product.t()
Build an ultra-rapid OPS SP3 product.
Ultra-rapid products are two-day (02D) files issued several times per day,
with roughly one observed day and one predicted day. Pass a Date with an
explicit issue: (defaults to "0000"), or pass a NaiveDateTime /
DateTime target and Orbis will select the latest issue not after that time.
If :available_issues is supplied, selection falls back to the newest issue
present in that list.
Examples
iex> p = Orbis.GNSS.Data.ops_ultra_sp3(:igs_ult, ~D[2024-09-03], issue: "0600")
iex> Orbis.GNSS.Data.Product.canonical_filename(p)
{:ok, "IGS0OPSULT_20242470600_02D_15M_ORB.SP3"}
iex> available = [{~D[2024-09-03], "0000"}, {~D[2024-09-03], "0600"}]
iex> p = Orbis.GNSS.Data.ops_ultra_sp3(:cod_ult, ~N[2024-09-03 13:00:00], available_issues: available)
iex> Orbis.GNSS.Data.Product.canonical_filename(p)
{:ok, "COD0OPSULT_20242470600_02D_05M_ORB.SP3"}
@spec product(atom(), atom(), Date.t(), String.t()) :: {:ok, Orbis.GNSS.Data.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 product(atom(), atom(), Date.t(), String.t(), keyword()) :: {:ok, Orbis.GNSS.Data.Product.t()} | {:error, {:unsupported_product, term()}}
Build a Product for any center/content/date/sample with product-specific
options such as issue: "0600" for ultra-rapid products.
@spec sp3( Orbis.GNSS.Data.Product.t(), keyword() ) :: {:ok, Orbis.GNSS.SP3.t()} | error()
Fetch an SP3 product and load it into an Orbis.GNSS.SP3 handle.
Equivalent to fetch/2 followed by Orbis.GNSS.SP3.load/1. Returns
{:ok, %Orbis.GNSS.SP3{}} or a typed error.
@spec station_obs(String.t(), Date.t(), keyword()) :: Orbis.GNSS.Data.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.GNSS.Data.station_obs("ESBC00DNK", ~D[2020-06-25])
iex> Orbis.GNSS.Data.Product.canonical_filename(p)
{:ok, "ESBC00DNK_R_20201770000_01D_30S_MO.crx"}