Skip to content

DHI.Services.USGS — Internal Guide (Time Series)

Provider for USGS Daily Values (DV) from waterservices.usgs.gov into the unified DHI.Services.TimeSeries model. Covers: repository behavior, ID rules, date scoping, flags/metadata, EUM mapping & conversion, client/query details, Web API wiring.


What this provider gives you

Scope Class Base type Read Write Grouped Notes
USGS Daily Values TimeSeriesRepository BaseTimeSeriesRepository<string,double> Targets DV endpoint; yields TimeSeries<string,double> with flags + rich metadata.

Key traits

  • ID format: {AgencyCode}:{SiteNumber}:{ParameterCode}:{StatisticsCode} (e.g., USGS:01100000:00060:00003).
  • Date window: optional defaults from the repository’s base address (startDT,endDT; case-insensitive). If omitted, USGS applies its own defaults.
  • Flags: USGS qualifiers (A, P, e, …) preserved per sample in DataWFlag.Flags.
  • Units/quantity: auto-mapped to EUM via UsgsToEum (with conversion factor when needed).
  • Format: WaterML 1.1 only; other formats throw NotSupportedException.

Quick start

using DHI.Services.Provider.USGS;
using DHI.Services.TimeSeries;

var repo = new TimeSeriesRepository(
  "https://waterservices.usgs.gov/nwis/dv/?startDT=2024-01-01&endDT=2024-12-31");

var id = "USGS:11458000:00060:00003"; // Agency:Site:Parameter:Statistic

var tsMaybe = repo.Get(id);
if (!tsMaybe.HasValue) return; // HTTP error or not found (see behavior below)

var ts    = (UsgsTimeSeries)tsMaybe.Value;
var data  = ts.Data;                  // ITimeSeriesData<double>
var flags = ts.DataWFlag.Flags;       // qualifiers per value

Console.WriteLine($"{ts.Name} [{ts.Quantity} / {ts.Unit}]  points={data.Count}");

Repository construction & default dates

// Any DV base URL is fine; query here establishes defaults for Get()
var repo = new TimeSeriesRepository(
  "https://waterservices.usgs.gov/nwis/dv/?startDT=2025-01-01&endDT=2025-06-30");
  • The constructor:
    • Accepts either a plain base URL (…/dv/) or one with a query string.
    • Parses startDT / endDT from the base address (case-insensitive) and stores them as default window.
    • Validates base address non-null; throws ArgumentNullException when null.
    • Parses dates as yyyy-MM-dd; bad formats throw a FormatException with a helpful message.
  • The underlying HTTP client enables gzip/deflate and enforces success status.

Time series IDs (validation)

Constructed/validated by UsgsTimeSeriesId:

Field Constraints (regex) Example
AgencyCode ^[A-Z0-9]{2,20}$ USGS
SiteNumber ^[0-9]{8,20}$ 01100000
ParameterCode ^[0-9]{5}$ 00060
StatisticsCode ^[0-9]{5}$ 00003
  • The raw string is upper-cased, trimmed, and split by :.
  • Any violation throws ArgumentException.

Reading APIs (per ID)

Maybe<TimeSeries<string,double>> Get(string id);

Maybe<ITimeSeriesData<double>> GetValues(string id);

Maybe<DataPoint<double>> GetValue(string id, DateTime at);
Maybe<DataPoint<double>> GetFirstValueAfter(string id, DateTime t);
Maybe<DataPoint<double>> GetLastValueBefore(string id, DateTime t);

Behavior

  • Get(id) composes a DV request using the parsed ID (+ default dates if present), fetches WaterML 1.1, and hydrates a UsgsTimeSeries.
  • On HttpRequestException or “time series not found in response”, the repo returns Maybe.Empty (exceptions are swallowed deliberately).
  • The first/last/value helpers call Get(id) internally and then search the hydrated series. Always check HasValue before accessing.

Guard array bounds if you replicate the “search index” patterns yourself; the repository assumes non-empty series.


What comes back (data, flags, metadata)

When a UsgsTimeSeries is hydrated:

  • DataTimeSeriesDataWFlag<double,string>
    • DateTimes + Values (double?)
    • Flags = USGS qualifiers per sample (DataWFlag.Flags[i])
  • EUM mapping from UsgsToEum:
    • QuantityeumItem
    • UniteumUnit
    • DataType ← mapped from USGS value type (e.g., “Step Accumulated”)
    • Dimension ← dimensionality string if known
  • Metadata (set on first hydration; selected keys):
    • variableCode, variableName, variableDescription, unitCode, value valueType
    • siteName, siteCode, Latitude, Longitude
    • timeZoneInfo/* (includes parsed default time zone from offset, preferring US & Canada zones when offsets match)
    • qualifier/<code> human-readable descriptions
    • note/<title>, allDisclaimers
    • noDataValue
    • Always updated: lastHydratedAt (UTC), lastQueryUrl, queryUrlHistory (append-only)

EUM mapping & value conversion (UsgsToEum)

  • Maps USGS parameter codesEUM (item, unit, default data type, dimension).
  • Exposes per-code conversion factor Factor = multiplier / divider (applied only when ≠ 1).

Examples:

// Lookup EUM info
var m = UsgsToEum.toEumItem("00060"); // discharge
Console.WriteLine($"item={m["eumItem"]}, unit={m["eumUnit"]}, type={m["valueType"]}");

// Convert values explicitly (if you want numeric values in EUM)
var eumValues = UsgsToEum.ConvertValuesOrNulls(ts.Data.Values, "00060");

The provider already sets EUM fields on the TimeSeries entity. Only convert numeric values if you need the numbers themselves in EUM.

Supported codes See the _lookup dictionary in UsgsToEum (includes flow, stage, temperature, precipitation, etc.) — unknown codes fall back to a default.


Building requests explicitly (advanced)

When you need multi-site, spatial filters, or ad-hoc windows, use the client & query parameters directly.

var client = new UsgsWaterServicesClient("https://waterservices.usgs.gov/nwis/dv/");

// At least one “major filter” must be supplied in this ctor (sites/stateCd/huc/bBox/countyCd)
var qp = new UsgsWaterServicesQueryParameters(sites: "11458000,11467890");
// qp.SetTimeRange(start, end)  or  qp.SetTimeRange(daysBackFromToday)

var url = client.BuildUrl(qp.ToDictionary()); // fully composed URL
var xml = client.FetchDailyValues(qp);        // WaterML 1.1 as string

var ts = new UsgsTimeSeries("USGS:11458000:00060:00003", "USGS:11458000:00060:00003");
client.FetchDailyValuesToTimeSeries(qp, ts, ignoreTimeSeriesNotFound: false);

Notes

  • UsgsWaterServicesQueryParameters adds format=waterml,1.1 automatically.
  • FetchDailyValuesToTimeSeries populates only if it finds an exact <timeSeries name="…"> matching ts.Id in the response.
  • If the response isn’t WaterML 1.1, hydration throws NotSupportedException.

Web API “Connections” wiring (example)

{
  "$type": "System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[DHI.Services.IConnection, DHI.Services]], mscorlib",

  "usgs-dv": {
    "$type": "DHI.Services.TimeSeries.WebApi.TimeSeriesServiceConnection, DHI.Services.TimeSeries.WebApi",
    "ConnectionString": "https://waterservices.usgs.gov/nwis/dv/?startDT=2024-01-01&endDT=2024-12-31",
    "RepositoryType": "DHI.Services.Provider.USGS.TimeSeriesRepository, DHI.Services.Provider.USGS",
    "Name": "USGS Daily Values",
    "Id": "usgs-dv"
  }
}

Typical HTTP calls (URL-encode :):

GET /api/timeseries/usgs-dv/USGS%3A11458000%3A00060%3A00003/values
GET /api/timeseries/usgs-dv/USGS%3A11458000%3A00060%3A00003/value?at=2024-06-01
GET /api/timeseries/usgs-dv/USGS%3A11458000%3A00060%3A00003/firstValueAfter?at=2024-06-01
GET /api/timeseries/usgs-dv/USGS%3A11458000%3A00060%3A00003/lastValueBefore?at=2024-06-01

Troubleshooting & guidance

  • Format: Only WaterML 1.1 is supported.
  • Date windows: Prefer bounded startDT/endDT to reduce payloads and avoid USGS defaults.
  • Empty results: Get(id) returns empty Maybe on HTTP errors or “series not found”.
  • Qualifiers: Inspect DataWFlag.Flags[i]; human-readable texts live under qualifier/<code> in metadata.
  • Units: EUM fields are set on the entity; convert values with UsgsToEum only when you need numeric value conversion.
  • Index guards: When scanning for first/last values, check bounds before indexing.

End-to-end example

var repo = new TimeSeriesRepository(
  "https://waterservices.usgs.gov/nwis/dv/?startDT=2024-01-01&endDT=2024-03-31");

var id = "USGS:11458000:00060:00003";

var tsMaybe = repo.Get(id);
if (!tsMaybe.HasValue) throw new Exception("USGS series not found.");

var ts = (UsgsTimeSeries)tsMaybe.Value;

Console.WriteLine($"{ts.Metadata["siteName"]} ({ts.Metadata["siteCode"]})");
Console.WriteLine($"{ts.Metadata["variableName"]} [{ts.Unit}] — {ts.DataType}");

var data  = ts.Data;
var flags = ts.DataWFlag.Flags;

for (int i = 0; i < Math.Min(data.Count, 3); i++)
{
  Console.WriteLine($"{data.DateTimes[i]:yyyy-MM-dd}: {data.Values[i]} ({flags[i]})");
}

// Convert numeric values to EUM if desired
var eumVals = UsgsToEum.ConvertValuesOrNulls(data.Values, ts.Metadata["variableCode"].ToString());