usgs-ruby 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop.yml +87 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +256 -0
- data/Rakefile +24 -0
- data/docs/Usgs/Client.html +563 -0
- data/docs/Usgs/DailyValues.html +338 -0
- data/docs/Usgs/InstantaneousValues.html +329 -0
- data/docs/Usgs/Models/Reading.html +1124 -0
- data/docs/Usgs/Models/Site.html +2030 -0
- data/docs/Usgs/Models/Statistic.html +3981 -0
- data/docs/Usgs/Models.html +117 -0
- data/docs/Usgs/Parser.html +346 -0
- data/docs/Usgs/Parsers/RdbParser.html +354 -0
- data/docs/Usgs/Parsers/SiteParser.html +174 -0
- data/docs/Usgs/Parsers/StatisticsParser.html +228 -0
- data/docs/Usgs/Parsers/TimeSeriesParser.html +189 -0
- data/docs/Usgs/Parsers.html +117 -0
- data/docs/Usgs/Site.html +445 -0
- data/docs/Usgs/Statistics.html +335 -0
- data/docs/Usgs/Utils.html +357 -0
- data/docs/Usgs.html +303 -0
- data/docs/_index.html +286 -0
- data/docs/class_list.html +54 -0
- data/docs/css/common.css +1 -0
- data/docs/css/full_list.css +58 -0
- data/docs/css/style.css +503 -0
- data/docs/file.README.html +111 -0
- data/docs/file_list.html +59 -0
- data/docs/frames.html +22 -0
- data/docs/index.html +111 -0
- data/docs/js/app.js +344 -0
- data/docs/js/full_list.js +242 -0
- data/docs/js/jquery.js +4 -0
- data/docs/method_list.html +598 -0
- data/docs/top-level-namespace.html +110 -0
- data/lib/usgs/client.rb +62 -0
- data/lib/usgs/daily_values.rb +38 -0
- data/lib/usgs/instantaneous_values.rb +35 -0
- data/lib/usgs/models/reading.rb +43 -0
- data/lib/usgs/models/site.rb +61 -0
- data/lib/usgs/models/statistic.rb +95 -0
- data/lib/usgs/parser.rb +23 -0
- data/lib/usgs/parsers/rdb_parser.rb +55 -0
- data/lib/usgs/parsers/site_parser.rb +13 -0
- data/lib/usgs/parsers/statistics_parser.rb +40 -0
- data/lib/usgs/parsers/time_series_parser.rb +54 -0
- data/lib/usgs/site.rb +50 -0
- data/lib/usgs/statistics.rb +42 -0
- data/lib/usgs/utils.rb +50 -0
- data/lib/usgs/version.rb +5 -0
- data/lib/usgs.rb +28 -0
- data/sig/usgs/ruby.rbs +6 -0
- data/usgs-ruby.gemspec +48 -0
- metadata +231 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>
|
|
7
|
+
Top Level Namespace
|
|
8
|
+
|
|
9
|
+
— Documentation by YARD 0.9.37
|
|
10
|
+
|
|
11
|
+
</title>
|
|
12
|
+
|
|
13
|
+
<link rel="stylesheet" href="css/style.css" type="text/css" />
|
|
14
|
+
|
|
15
|
+
<link rel="stylesheet" href="css/common.css" type="text/css" />
|
|
16
|
+
|
|
17
|
+
<script type="text/javascript">
|
|
18
|
+
pathId = "";
|
|
19
|
+
relpath = '';
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
<script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
|
|
24
|
+
|
|
25
|
+
<script type="text/javascript" charset="utf-8" src="js/app.js"></script>
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
</head>
|
|
29
|
+
<body>
|
|
30
|
+
<div class="nav_wrap">
|
|
31
|
+
<iframe id="nav" src="class_list.html?1"></iframe>
|
|
32
|
+
<div id="resizer"></div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div id="main" tabindex="-1">
|
|
36
|
+
<div id="header">
|
|
37
|
+
<div id="menu">
|
|
38
|
+
|
|
39
|
+
<a href="_index.html">Index</a> »
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
<span class="title">Top Level Namespace</span>
|
|
43
|
+
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div id="search">
|
|
47
|
+
|
|
48
|
+
<a class="full_list_link" id="class_list_link"
|
|
49
|
+
href="class_list.html">
|
|
50
|
+
|
|
51
|
+
<svg width="24" height="24">
|
|
52
|
+
<rect x="0" y="4" width="24" height="4" rx="1" ry="1"></rect>
|
|
53
|
+
<rect x="0" y="12" width="24" height="4" rx="1" ry="1"></rect>
|
|
54
|
+
<rect x="0" y="20" width="24" height="4" rx="1" ry="1"></rect>
|
|
55
|
+
</svg>
|
|
56
|
+
</a>
|
|
57
|
+
|
|
58
|
+
</div>
|
|
59
|
+
<div class="clear"></div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div id="content"><h1>Top Level Namespace
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
</h1>
|
|
67
|
+
<div class="box_info">
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<h2>Defined Under Namespace</h2>
|
|
82
|
+
<p class="children">
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
<strong class="modules">Modules:</strong> <span class='object_link'><a href="Usgs.html" title="Usgs (module)">Usgs</a></span>
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
</p>
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div id="footer">
|
|
103
|
+
Generated on Mon Dec 15 21:52:18 2025 by
|
|
104
|
+
<a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
|
|
105
|
+
0.9.37 (ruby-3.2.2).
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
</div>
|
|
109
|
+
</body>
|
|
110
|
+
</html>
|
data/lib/usgs/client.rb
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Usgs
|
|
4
|
+
class Client
|
|
5
|
+
include DailyValues
|
|
6
|
+
include InstantaneousValues
|
|
7
|
+
include Site
|
|
8
|
+
include Statistics
|
|
9
|
+
|
|
10
|
+
attr_reader :timeout, :user_agent, :debug
|
|
11
|
+
|
|
12
|
+
def initialize(timeout: 30, user_agent: "usgs-ruby/#{Usgs::VERSION}", debug: nil)
|
|
13
|
+
@timeout = timeout
|
|
14
|
+
@user_agent = user_agent
|
|
15
|
+
@debug = debug.nil? ? Usgs.config.debug : debug
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Base URL for USGS Water Services
|
|
19
|
+
def base_url
|
|
20
|
+
"https://waterservices.usgs.gov/nwis"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Public: Perform GET and return response from API
|
|
24
|
+
def api_get(path, query = {})
|
|
25
|
+
query = query.compact
|
|
26
|
+
url = "#{base_url}#{path}"
|
|
27
|
+
|
|
28
|
+
fetch_url(url, query: query, timeout: timeout, user_agent: user_agent)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def fetch_url(url, query: {}, timeout: 30, user_agent: nil)
|
|
34
|
+
uri = URI(url)
|
|
35
|
+
uri.query = URI.encode_www_form(query).gsub("+", "%20") unless query.empty?
|
|
36
|
+
|
|
37
|
+
puts "\n=== USGS Request ===\n#{uri}\n====================\n" if @debug
|
|
38
|
+
|
|
39
|
+
http_get(uri, timeout: timeout, user_agent: user_agent)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def http_get(uri, timeout: 30, user_agent: nil)
|
|
43
|
+
Net::HTTP.start(
|
|
44
|
+
uri.host,
|
|
45
|
+
uri.port,
|
|
46
|
+
use_ssl: true,
|
|
47
|
+
open_timeout: timeout,
|
|
48
|
+
read_timeout: timeout
|
|
49
|
+
) do |http|
|
|
50
|
+
request = Net::HTTP::Get.new(uri)
|
|
51
|
+
request["User-Agent"] = user_agent if user_agent
|
|
52
|
+
request["Accept"] = "application/json"
|
|
53
|
+
|
|
54
|
+
response = http.request(request)
|
|
55
|
+
|
|
56
|
+
raise "USGS API Error #{response.code}: #{response.message}\n#{response.body}" unless response.is_a?(Net::HTTPSuccess)
|
|
57
|
+
|
|
58
|
+
response
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# lib/usgs/daily_values.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Usgs
|
|
5
|
+
module DailyValues
|
|
6
|
+
include Utils
|
|
7
|
+
# Fetch daily values (DV) from USGS NWIS
|
|
8
|
+
#
|
|
9
|
+
# @param sites [String, Array<String>] USGS site ID(s)
|
|
10
|
+
# @param parameter_cd [Symbol, String, Array] e.g. :discharge
|
|
11
|
+
# @param start_date [Date, String, nil] Start date — defaults to 10 years ago if omitted
|
|
12
|
+
# @param end_date [Date, String, nil] End date — defaults to today
|
|
13
|
+
#
|
|
14
|
+
# @return [Array<Usgs::Models::Reading>]
|
|
15
|
+
#
|
|
16
|
+
# @example Most recent year
|
|
17
|
+
# Usgs.client.get_dv(sites: "06754000", parameter_cd: :discharge)
|
|
18
|
+
#
|
|
19
|
+
# @example Full POR
|
|
20
|
+
# Usgs.client.get_dv(sites: "06754000", parameter_cd: :discharge, start_date: "1900-01-01")
|
|
21
|
+
#
|
|
22
|
+
def get_dv(sites:, parameter_cd: nil, start_date: nil, end_date: nil)
|
|
23
|
+
site_list = Array(sites).join(",")
|
|
24
|
+
param_list = resolve_parameter_codes(parameter_cd)
|
|
25
|
+
|
|
26
|
+
query = {
|
|
27
|
+
format: "json",
|
|
28
|
+
sites: site_list,
|
|
29
|
+
parameterCd: param_list,
|
|
30
|
+
startDT: format_date(start_date || (Time.now.utc - (48 * 60 * 60))),
|
|
31
|
+
endDT: format_date(end_date || Time.now.utc)
|
|
32
|
+
}.compact
|
|
33
|
+
|
|
34
|
+
response = api_get("/dv/", query)
|
|
35
|
+
Parser.parse_time_series_values(JSON.parse(response.body))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Usgs
|
|
4
|
+
module InstantaneousValues
|
|
5
|
+
include Utils
|
|
6
|
+
# Fetch instantaneous values (IV) from USGS NWIS
|
|
7
|
+
#
|
|
8
|
+
# @param sites [String, Array<String>] One or more USGS site IDs
|
|
9
|
+
# @param parameter_cd [Symbol, String, Array] e.g. :discharge, "00060", or [:discharge, :gage_height]
|
|
10
|
+
# @param start_date [DateTime, Date, Time, String, nil] Start time
|
|
11
|
+
# @param end_date [DateTime, Date, Time, String, nil] End time (default: now)
|
|
12
|
+
#
|
|
13
|
+
# @return [Array<Usgs::Models::Reading>]
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# Usgs.client.get_iv(sites: "06754000", parameter_cd: :discharge, start_date: 1.day.ago)
|
|
17
|
+
#
|
|
18
|
+
def get_iv(sites:, parameter_cd: nil, start_date: nil, end_date: nil)
|
|
19
|
+
site_list = Array(sites).join(",")
|
|
20
|
+
param_list = resolve_parameter_codes(parameter_cd)
|
|
21
|
+
|
|
22
|
+
query = {
|
|
23
|
+
format: "json",
|
|
24
|
+
sites: site_list,
|
|
25
|
+
parameterCd: param_list,
|
|
26
|
+
# Default to the the last 24hrs if not filled out
|
|
27
|
+
startDT: format_datetime(start_date || (Time.now.utc - (24 * 60 * 60))),
|
|
28
|
+
endDT: format_datetime(end_date || Time.now.utc)
|
|
29
|
+
}.compact
|
|
30
|
+
|
|
31
|
+
response = api_get("/iv/", query)
|
|
32
|
+
Parser.parse_time_series_values(JSON.parse(response.body))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Usgs
|
|
4
|
+
module Models
|
|
5
|
+
# Represents a USGS reading/measurement
|
|
6
|
+
#
|
|
7
|
+
# @!attribute [rw] site_no
|
|
8
|
+
# @return [String] USGS site number
|
|
9
|
+
# @!attribute [rw] parameter_cd
|
|
10
|
+
# @return [String] Parameter code
|
|
11
|
+
# @!attribute [rw] datetime
|
|
12
|
+
# @return [String] Date and time of reading
|
|
13
|
+
# @!attribute [rw] value
|
|
14
|
+
# @return [Float, nil] Measured value
|
|
15
|
+
# @!attribute [rw] qualifiers
|
|
16
|
+
# @return [Array<String>] Quality/approval codes
|
|
17
|
+
# @!attribute [rw] unit
|
|
18
|
+
# @return [String] Unit of measurement
|
|
19
|
+
# @!attribute [rw] metadata
|
|
20
|
+
# @return [Hash] Additional metadata
|
|
21
|
+
class Reading
|
|
22
|
+
ATTRIBUTES = %i[
|
|
23
|
+
site_no
|
|
24
|
+
parameter_cd
|
|
25
|
+
datetime
|
|
26
|
+
value
|
|
27
|
+
qualifiers
|
|
28
|
+
unit
|
|
29
|
+
metadata
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
attr_accessor(*ATTRIBUTES) # :nodoc:
|
|
33
|
+
|
|
34
|
+
def initialize(data = {})
|
|
35
|
+
data[:metadata] ||= {}
|
|
36
|
+
attrs = data.is_a?(Hash) ? data : {}
|
|
37
|
+
ATTRIBUTES.each do |attr|
|
|
38
|
+
instance_variable_set(:"@#{attr}", attrs[attr]) if attrs.key?(attr)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Usgs
|
|
4
|
+
module Models
|
|
5
|
+
# Represents a USGS monitoring site/station
|
|
6
|
+
#
|
|
7
|
+
# @!attribute [rw] agency_cd
|
|
8
|
+
# @return [String] Agency code
|
|
9
|
+
# @!attribute [rw] site_no
|
|
10
|
+
# @return [String] USGS site number
|
|
11
|
+
# @!attribute [rw] station_nm
|
|
12
|
+
# @return [String] Station name
|
|
13
|
+
# @!attribute [rw] site_tp_cd
|
|
14
|
+
# @return [String] Site type code
|
|
15
|
+
# @!attribute [rw] dec_lat_va
|
|
16
|
+
# @return [Float, nil] Decimal latitude
|
|
17
|
+
# @!attribute [rw] dec_long_va
|
|
18
|
+
# @return [Float, nil] Decimal longitude
|
|
19
|
+
# @!attribute [rw] coord_acy_cd
|
|
20
|
+
# @return [String] Coordinate accuracy code
|
|
21
|
+
# @!attribute [rw] dec_coord_datum_cd
|
|
22
|
+
# @return [String] Decimal coordinate datum code
|
|
23
|
+
# @!attribute [rw] alt_va
|
|
24
|
+
# @return [String] Altitude value
|
|
25
|
+
# @!attribute [rw] alt_acy_va
|
|
26
|
+
# @return [String] Altitude accuracy value
|
|
27
|
+
# @!attribute [rw] alt_datum_cd
|
|
28
|
+
# @return [String] Altitude datum code
|
|
29
|
+
# @!attribute [rw] huc_cd
|
|
30
|
+
# @return [String] Hydrologic unit code
|
|
31
|
+
# @!attribute [rw] metadata
|
|
32
|
+
# @return [Hash] Additional metadata
|
|
33
|
+
class Site
|
|
34
|
+
ATTRIBUTES = %i[
|
|
35
|
+
agency_cd
|
|
36
|
+
site_no
|
|
37
|
+
station_nm
|
|
38
|
+
site_tp_cd
|
|
39
|
+
dec_lat_va
|
|
40
|
+
dec_long_va
|
|
41
|
+
coord_acy_cd
|
|
42
|
+
dec_coord_datum_cd
|
|
43
|
+
alt_va
|
|
44
|
+
alt_acy_va
|
|
45
|
+
alt_datum_cd
|
|
46
|
+
huc_cd
|
|
47
|
+
metadata
|
|
48
|
+
].freeze
|
|
49
|
+
|
|
50
|
+
attr_accessor(*ATTRIBUTES) # :nodoc:
|
|
51
|
+
|
|
52
|
+
def initialize(attrs = {})
|
|
53
|
+
attrs = {} unless attrs.is_a?(Hash)
|
|
54
|
+
attrs[:metadata] ||= {}
|
|
55
|
+
ATTRIBUTES.each do |attr|
|
|
56
|
+
instance_variable_set(:"@#{attr}", attrs[attr]) if attrs.key?(attr)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Usgs
|
|
4
|
+
module Models
|
|
5
|
+
# Represents a USGS statistic record
|
|
6
|
+
#
|
|
7
|
+
# @!attribute [rw] site_no
|
|
8
|
+
# @return [String] USGS site number
|
|
9
|
+
# @!attribute [rw] parameter_cd
|
|
10
|
+
# @return [String] Parameter code
|
|
11
|
+
# @!attribute [rw] month_nu
|
|
12
|
+
# @return [Float, nil] Month number
|
|
13
|
+
# @!attribute [rw] day_nu
|
|
14
|
+
# @return [Float, nil] Day number
|
|
15
|
+
# @!attribute [rw] begin_yr
|
|
16
|
+
# @return [Float, nil] Beginning year
|
|
17
|
+
# @!attribute [rw] end_yr
|
|
18
|
+
# @return [Float, nil] Ending year
|
|
19
|
+
# @!attribute [rw] count_nu
|
|
20
|
+
# @return [Float, nil] Count of values
|
|
21
|
+
# @!attribute [rw] mean_va
|
|
22
|
+
# @return [Float, nil] Mean value
|
|
23
|
+
# @!attribute [rw] max_va
|
|
24
|
+
# @return [Float, nil] Maximum value
|
|
25
|
+
# @!attribute [rw] max_va_yr
|
|
26
|
+
# @return [Float, nil] Year of maximum value
|
|
27
|
+
# @!attribute [rw] min_va
|
|
28
|
+
# @return [Float, nil] Minimum value
|
|
29
|
+
# @!attribute [rw] min_va_yr
|
|
30
|
+
# @return [Float, nil] Year of minimum value
|
|
31
|
+
# @!attribute [rw] p05_va
|
|
32
|
+
# @return [Float, nil] 5th percentile value
|
|
33
|
+
# @!attribute [rw] p10_va
|
|
34
|
+
# @return [Float, nil] 10th percentile value
|
|
35
|
+
# @!attribute [rw] p20_va
|
|
36
|
+
# @return [Float, nil] 20th percentile value
|
|
37
|
+
# @!attribute [rw] p25_va
|
|
38
|
+
# @return [Float, nil] 25th percentile value
|
|
39
|
+
# @!attribute [rw] p50_va
|
|
40
|
+
# @return [Float, nil] 50th percentile (median) value
|
|
41
|
+
# @!attribute [rw] p75_va
|
|
42
|
+
# @return [Float, nil] 75th percentile value
|
|
43
|
+
# @!attribute [rw] p80_va
|
|
44
|
+
# @return [Float, nil] 80th percentile value
|
|
45
|
+
# @!attribute [rw] p90_va
|
|
46
|
+
# @return [Float, nil] 90th percentile value
|
|
47
|
+
# @!attribute [rw] p95_va
|
|
48
|
+
# @return [Float, nil] 95th percentile value
|
|
49
|
+
# @!attribute [rw] metadata
|
|
50
|
+
# @return [Hash] Additional metadata
|
|
51
|
+
class Statistic
|
|
52
|
+
ATTRIBUTES = %i[
|
|
53
|
+
site_no
|
|
54
|
+
parameter_cd
|
|
55
|
+
month_nu
|
|
56
|
+
day_nu
|
|
57
|
+
begin_yr
|
|
58
|
+
end_yr
|
|
59
|
+
count_nu
|
|
60
|
+
mean_va
|
|
61
|
+
max_va
|
|
62
|
+
max_va_yr
|
|
63
|
+
min_va
|
|
64
|
+
min_va_yr
|
|
65
|
+
p05_va
|
|
66
|
+
p10_va
|
|
67
|
+
p20_va
|
|
68
|
+
p25_va
|
|
69
|
+
p50_va
|
|
70
|
+
p75_va
|
|
71
|
+
p80_va
|
|
72
|
+
p90_va
|
|
73
|
+
p95_va
|
|
74
|
+
metadata
|
|
75
|
+
].freeze
|
|
76
|
+
|
|
77
|
+
attr_accessor(*ATTRIBUTES) # :nodoc:
|
|
78
|
+
|
|
79
|
+
def initialize(data = {})
|
|
80
|
+
data[:metadata] ||= {}
|
|
81
|
+
attrs = data.is_a?(Hash) ? data : {}
|
|
82
|
+
ATTRIBUTES.each do |attr|
|
|
83
|
+
value = attrs[attr]
|
|
84
|
+
|
|
85
|
+
if value.is_a?(String) && attr.to_s.end_with?("_va", "_yr", "_nu")
|
|
86
|
+
stripped = value.strip
|
|
87
|
+
value = stripped.empty? || stripped == "." ? nil : stripped.to_f
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
instance_variable_set(:"@#{attr}", value) if attrs.key?(attr)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
data/lib/usgs/parser.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Usgs
|
|
4
|
+
class Parser
|
|
5
|
+
class << self
|
|
6
|
+
def parse_sites(response)
|
|
7
|
+
Parsers::RdbParser.parse(response)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def parse_time_series(response, timescale: :instantaneous)
|
|
11
|
+
# Parsers::TimeSeriesParser.parse(response, timescale: timescale)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def parse_time_series_values(response)
|
|
15
|
+
Parsers::TimeSeriesParser.parse(response)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def parse_statistics(response)
|
|
19
|
+
Parsers::StatisticsParser.parse(response)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Usgs
|
|
4
|
+
module Parsers
|
|
5
|
+
module RdbParser
|
|
6
|
+
class << self
|
|
7
|
+
# Parse any USGS RDB response (format=rdb or rdb,1.0)
|
|
8
|
+
#
|
|
9
|
+
# @param text [String] Raw RDB text
|
|
10
|
+
# @return [Array<Hash>] Rows with symbolized column names
|
|
11
|
+
def parse(text)
|
|
12
|
+
lines = text.lines.map(&:rstrip)
|
|
13
|
+
|
|
14
|
+
# Drop all comment lines (#)
|
|
15
|
+
data_lines = lines.drop_while { |l| l.start_with?("#") }
|
|
16
|
+
|
|
17
|
+
return [] if data_lines.empty?
|
|
18
|
+
|
|
19
|
+
field_names_line = data_lines[0]
|
|
20
|
+
|
|
21
|
+
data_start_index = if data_lines[1]&.match?(/\A(\d+[sd])\t/)
|
|
22
|
+
2
|
|
23
|
+
else
|
|
24
|
+
1
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
column_names = field_names_line.split("\t")
|
|
28
|
+
data_lines[data_start_index..].map do |line|
|
|
29
|
+
next if line.empty? || line.start_with?("#")
|
|
30
|
+
|
|
31
|
+
values = line.split("\t")
|
|
32
|
+
column_names.zip(values).to_h.transform_keys { |k| k.strip.to_sym }
|
|
33
|
+
end.compact
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# For time series (iv, dv, etc.)
|
|
37
|
+
def parse_time_series(text)
|
|
38
|
+
rows = parse(text)
|
|
39
|
+
rows.map do |row|
|
|
40
|
+
value = row[:value]&.strip
|
|
41
|
+
value = nil if value.nil? || value == "" || value == "-999999" || value.downcase.include?("ice")
|
|
42
|
+
|
|
43
|
+
{
|
|
44
|
+
site_no: row[:site_no]&.strip,
|
|
45
|
+
datetime: row[:datetime] || row[:dateTime],
|
|
46
|
+
value: value&.to_f,
|
|
47
|
+
code: row[:value_cd] || row[:qualifiers],
|
|
48
|
+
parameter_cd: row[:parameter_cd] || row[:parm_cd]
|
|
49
|
+
}.compact
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Usgs
|
|
4
|
+
module Parsers
|
|
5
|
+
module StatisticsParser
|
|
6
|
+
class << self
|
|
7
|
+
def parse(rdb_text)
|
|
8
|
+
rows = Parsers::RdbParser.parse(rdb_text)
|
|
9
|
+
|
|
10
|
+
rows.map do |row|
|
|
11
|
+
{
|
|
12
|
+
site_no: row[:site_no]&.strip,
|
|
13
|
+
parameter_cd: row[:parameter_cd]&.strip,
|
|
14
|
+
month_nu: row[:month_nu]&.to_i,
|
|
15
|
+
day_nu: row[:day_nu]&.to_i,
|
|
16
|
+
begin_yr: row[:begin_yr]&.to_i,
|
|
17
|
+
end_yr: row[:end_yr]&.to_i,
|
|
18
|
+
count_nu: row[:count_nu]&.to_i,
|
|
19
|
+
mean_va: row[:mean_va],
|
|
20
|
+
max_va: row[:max_va],
|
|
21
|
+
max_va_yr: row[:max_va_yr]&.to_i,
|
|
22
|
+
min_va: row[:min_va],
|
|
23
|
+
min_va_yr: row[:min_va_yr]&.to_i,
|
|
24
|
+
p05_va: row[:p05_va],
|
|
25
|
+
p10_va: row[:p10_va],
|
|
26
|
+
p20_va: row[:p20_va],
|
|
27
|
+
p25_va: row[:p25_va],
|
|
28
|
+
p50_va: row[:p50_va],
|
|
29
|
+
p75_va: row[:p75_va],
|
|
30
|
+
p80_va: row[:p80_va],
|
|
31
|
+
p90_va: row[:p90_va],
|
|
32
|
+
p95_va: row[:p95_va],
|
|
33
|
+
metadata: {}
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Usgs
|
|
4
|
+
module Parsers
|
|
5
|
+
module TimeSeriesParser
|
|
6
|
+
class << self
|
|
7
|
+
# Parse both Instantaneous Values (IV) and Daily Values (DV)
|
|
8
|
+
# They have identical JSON structure
|
|
9
|
+
def parse(response)
|
|
10
|
+
series_list = response.dig("value", "timeSeries") || []
|
|
11
|
+
series_list.flat_map { |series| parse_series(series) }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def parse_series(series)
|
|
17
|
+
info = series["sourceInfo"]
|
|
18
|
+
variable = series["variable"]
|
|
19
|
+
values = series["values"]&.first&.dig("value") || []
|
|
20
|
+
|
|
21
|
+
site_no = info.dig("siteCode", 0, "value")
|
|
22
|
+
parameter_cd = variable.dig("variableCode", 0, "value")
|
|
23
|
+
unit = variable.dig("unit", "unitCode")
|
|
24
|
+
|
|
25
|
+
values.map do |v|
|
|
26
|
+
raw_value = v["value"]
|
|
27
|
+
|
|
28
|
+
value = case raw_value
|
|
29
|
+
when nil, "", "-999999", "Ice", "Eqp", "Fld", "Bkw", "Rat"
|
|
30
|
+
nil
|
|
31
|
+
else
|
|
32
|
+
raw_value.to_f
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# For DV: "2025-12-04T00:00:00.000" → "2025-12-04"
|
|
36
|
+
# For IV: "2025-12-04T12:15:00.000-07:00" → full string (preserved)
|
|
37
|
+
datetime = v["dateTime"]
|
|
38
|
+
datetime = datetime[0..9] if datetime&.include?("T00:00:00.000")
|
|
39
|
+
|
|
40
|
+
{
|
|
41
|
+
site_no: site_no,
|
|
42
|
+
parameter_cd: parameter_cd,
|
|
43
|
+
datetime: datetime,
|
|
44
|
+
value: value,
|
|
45
|
+
qualifiers: v["qualifiers"],
|
|
46
|
+
unit: unit,
|
|
47
|
+
metadata: {}
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/usgs/site.rb
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Usgs
|
|
4
|
+
module Site
|
|
5
|
+
include Utils
|
|
6
|
+
|
|
7
|
+
# Fetch USGS monitoring locations (sites)
|
|
8
|
+
#
|
|
9
|
+
# @param state_cd [String] Two-letter state code (e.g., "CO")
|
|
10
|
+
# @param county_cd [String] FIPS county code (e.g., "08013")
|
|
11
|
+
# @param huc [String] 2-16 digit Hydrologic Unit Code
|
|
12
|
+
# @param bBox [String] "west,south,east,north" in decimal degrees
|
|
13
|
+
# @param site_name [String] Text search in station name
|
|
14
|
+
# @param site_type [String] e.g., "ST" (stream), "GW", "LK", "WE"
|
|
15
|
+
# @param site_status [String] "active", "inactive", "all" (default: "all")
|
|
16
|
+
# @param parameter_cd [String, Symbol, Array] e.g. :discharge, "00060", or [:discharge, :gage_height]
|
|
17
|
+
#
|
|
18
|
+
# @return [Array<Usgs::Models::Site>]
|
|
19
|
+
# @raise [ArgumentError] if no major filter is provided
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# Usgs.client.get_sites(state_cd: "CO", parameter_cd: :discharge)
|
|
23
|
+
#
|
|
24
|
+
def get_sites(state_cd: nil, county_cd: nil, huc: nil, b_box: nil, site_name: nil,
|
|
25
|
+
site_type: nil, site_status: "all", parameter_cd: nil)
|
|
26
|
+
query = {
|
|
27
|
+
format: "rdb,1.0",
|
|
28
|
+
stateCd: state_cd,
|
|
29
|
+
countyCd: county_cd,
|
|
30
|
+
huc: huc,
|
|
31
|
+
bBox: b_box,
|
|
32
|
+
siteName: site_name,
|
|
33
|
+
siteType: site_type,
|
|
34
|
+
siteStatus: site_status
|
|
35
|
+
}.compact
|
|
36
|
+
|
|
37
|
+
if parameter_cd
|
|
38
|
+
resolved = resolve_parameter_codes(parameter_cd)
|
|
39
|
+
query[:parameterCd] = resolved if resolved && !resolved.empty?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Validate for at least one major filter
|
|
43
|
+
major_keys = %i[stateCd countyCd huc bBox].map(&:to_sym)
|
|
44
|
+
raise ArgumentError, "You must provide at least one major filter: state_cd, county_cd, huc or b_box" if (query.keys & major_keys).empty?
|
|
45
|
+
|
|
46
|
+
raw = api_get("/site/", query)
|
|
47
|
+
Parser.parse_sites(raw.body).map { |row| Models::Site.new(row) }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|