@mailwoman/soil 9.2.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.
- package/README.md +221 -0
- package/lib/index.ts +474 -0
- package/lib/schema.ts +493 -0
- package/lib/scripts/ingest-chunk.ts +47 -0
- package/lib/sdk/acquire.ts +124 -0
- package/lib/sdk/build-soil.ts +778 -0
- package/lib/sdk/cell-tiers.ts +310 -0
- package/lib/sdk/cells.ts +224 -0
- package/lib/sdk/client.ts +273 -0
- package/lib/sdk/download.ts +161 -0
- package/lib/sdk/index.ts +20 -0
- package/lib/sdk/ingest-chunk.ts +182 -0
- package/lib/sdk/ingest.ts +271 -0
- package/lib/sdk/measure-resolutions.ts +81 -0
- package/lib/sdk/reduce.ts +375 -0
- package/lib/sdk/survey-area.ts +467 -0
- package/lib/sdk/tabular.ts +296 -0
- package/lib/sdk/verify.ts +369 -0
- package/lib/test-kit.ts +255 -0
- package/lib/vocabulary.ts +241 -0
- package/out/index.d.ts +199 -0
- package/out/index.d.ts.map +1 -0
- package/out/index.js +255 -0
- package/out/index.js.map +1 -0
- package/out/schema.d.ts +369 -0
- package/out/schema.d.ts.map +1 -0
- package/out/schema.js +186 -0
- package/out/schema.js.map +1 -0
- package/out/scripts/ingest-chunk.d.ts +11 -0
- package/out/scripts/ingest-chunk.d.ts.map +1 -0
- package/out/scripts/ingest-chunk.js +42 -0
- package/out/scripts/ingest-chunk.js.map +1 -0
- package/out/sdk/acquire.d.ts +60 -0
- package/out/sdk/acquire.d.ts.map +1 -0
- package/out/sdk/acquire.js +74 -0
- package/out/sdk/acquire.js.map +1 -0
- package/out/sdk/build-soil.d.ts +197 -0
- package/out/sdk/build-soil.d.ts.map +1 -0
- package/out/sdk/build-soil.js +434 -0
- package/out/sdk/build-soil.js.map +1 -0
- package/out/sdk/cell-tiers.d.ts +47 -0
- package/out/sdk/cell-tiers.d.ts.map +1 -0
- package/out/sdk/cell-tiers.js +194 -0
- package/out/sdk/cell-tiers.js.map +1 -0
- package/out/sdk/cells.d.ts +118 -0
- package/out/sdk/cells.d.ts.map +1 -0
- package/out/sdk/cells.js +140 -0
- package/out/sdk/cells.js.map +1 -0
- package/out/sdk/client.d.ts +120 -0
- package/out/sdk/client.d.ts.map +1 -0
- package/out/sdk/client.js +214 -0
- package/out/sdk/client.js.map +1 -0
- package/out/sdk/download.d.ts +84 -0
- package/out/sdk/download.d.ts.map +1 -0
- package/out/sdk/download.js +117 -0
- package/out/sdk/download.js.map +1 -0
- package/out/sdk/index.d.ts +20 -0
- package/out/sdk/index.d.ts.map +1 -0
- package/out/sdk/index.js +20 -0
- package/out/sdk/index.js.map +1 -0
- package/out/sdk/ingest-chunk.d.ts +73 -0
- package/out/sdk/ingest-chunk.d.ts.map +1 -0
- package/out/sdk/ingest-chunk.js +98 -0
- package/out/sdk/ingest-chunk.js.map +1 -0
- package/out/sdk/ingest.d.ts +132 -0
- package/out/sdk/ingest.d.ts.map +1 -0
- package/out/sdk/ingest.js +163 -0
- package/out/sdk/ingest.js.map +1 -0
- package/out/sdk/measure-resolutions.d.ts +39 -0
- package/out/sdk/measure-resolutions.d.ts.map +1 -0
- package/out/sdk/measure-resolutions.js +52 -0
- package/out/sdk/measure-resolutions.js.map +1 -0
- package/out/sdk/reduce.d.ts +122 -0
- package/out/sdk/reduce.d.ts.map +1 -0
- package/out/sdk/reduce.js +266 -0
- package/out/sdk/reduce.js.map +1 -0
- package/out/sdk/survey-area.d.ts +105 -0
- package/out/sdk/survey-area.d.ts.map +1 -0
- package/out/sdk/survey-area.js +346 -0
- package/out/sdk/survey-area.js.map +1 -0
- package/out/sdk/tabular.d.ts +106 -0
- package/out/sdk/tabular.d.ts.map +1 -0
- package/out/sdk/tabular.js +194 -0
- package/out/sdk/tabular.js.map +1 -0
- package/out/sdk/verify.d.ts +135 -0
- package/out/sdk/verify.d.ts.map +1 -0
- package/out/sdk/verify.js +221 -0
- package/out/sdk/verify.js.map +1 -0
- package/out/test-kit.d.ts +80 -0
- package/out/test-kit.d.ts.map +1 -0
- package/out/test-kit.js +228 -0
- package/out/test-kit.js.map +1 -0
- package/out/vocabulary.d.ts +179 -0
- package/out/vocabulary.d.ts.map +1 -0
- package/out/vocabulary.js +215 -0
- package/out/vocabulary.js.map +1 -0
- package/package.json +276 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Soil Data Access — NRCS's live SQL service, and the two things this layer asks it: which survey areas
|
|
7
|
+
* exist with what version date, and which map unit covers a point.
|
|
8
|
+
*
|
|
9
|
+
* THIS IS AN API REQUEST AND IT GOES THROUGH {@linkcode APIClient}. Small bodies, repeated calls, a
|
|
10
|
+
* third-party host with a server-side query timeout and no published rate limit — the pacing, bounded
|
|
11
|
+
* retry, response caching and `ResourceError` mapping are exactly what it needs. The survey-area
|
|
12
|
+
* ARCHIVES are not: they are 13 to 41 MB file transfers, they stream to disk on raw `fetch`, and
|
|
13
|
+
* `download.ts` says so in place.
|
|
14
|
+
*
|
|
15
|
+
* FAILURES COME BACK AS XML, INCLUDING ON A TIMEOUT, AND A JSON-ONLY PARSER MIS-READS THEM. A bad column,
|
|
16
|
+
* a blocked query and a query that exceeded the server's own timeout all return an OGC
|
|
17
|
+
* `ServiceExceptionReport` document. Measured messages: `Invalid query: Invalid column name
|
|
18
|
+
* 'nosuchcolumn'.` (HTTP 400), `Invalid query - access denied.`, and `Your query timed out.` — and the
|
|
19
|
+
* last one arrives on an HTTP 200. So every response is read as TEXT and checked for the report before
|
|
20
|
+
* anything tries to parse it as JSON. A client that branched on the status code alone would read a
|
|
21
|
+
* timeout as a successful empty answer, which is the exact shape of lie this program keeps writing down.
|
|
22
|
+
*
|
|
23
|
+
* SCHEMA INTROSPECTION IS REFUSED, SO THE COLUMN NAMES ARE THE PUBLISHED DATA DICTIONARY'S.
|
|
24
|
+
* `SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS` answers `Invalid query - access denied.` The
|
|
25
|
+
* columns this file names were each verified by querying them successfully.
|
|
26
|
+
*
|
|
27
|
+
* FRESHNESS IS `sacatalog.saverest` AND NEVER A LENGTH PROBE. The download host answers `HEAD` with HTTP
|
|
28
|
+
* 405 and IGNORES `Range` — a request with `Range: bytes=0-0` returned HTTP 200 and transferred the whole
|
|
29
|
+
* 27,598,377 bytes — so "just check the size" starts a real download. The tabular service answers the
|
|
30
|
+
* freshness question directly instead, and the version date it returns is what the archive's filename
|
|
31
|
+
* embeds.
|
|
32
|
+
*/
|
|
33
|
+
import { APIClient } from "@mailwoman/core/api";
|
|
34
|
+
import { buildDiskStorage } from "@mailwoman/core/api/disk-storage";
|
|
35
|
+
import { dataRootPath } from "@mailwoman/core/data-root";
|
|
36
|
+
import { parseJSONStrict } from "@mailwoman/core/objects";
|
|
37
|
+
import { decodeXML } from "entities";
|
|
38
|
+
import { saverestToISODate } from "#sdk/tabular";
|
|
39
|
+
// Re-exported so a caller branching on this client's failures needs exactly one import.
|
|
40
|
+
export { isTransientResourceError } from "@mailwoman/core/api";
|
|
41
|
+
export { ResourceError } from "@mailwoman/core/errors";
|
|
42
|
+
/**
|
|
43
|
+
* The tabular endpoint. Anonymous: no key, no account, and no rate-limit header on any observed response.
|
|
44
|
+
*/
|
|
45
|
+
export const SDA_POST_REST_URL = "https://sdmdataaccess.nrcs.usda.gov/Tabular/post.rest";
|
|
46
|
+
/**
|
|
47
|
+
* Minimum spacing between Soil Data Access requests, in milliseconds.
|
|
48
|
+
*
|
|
49
|
+
* NRCS publishes no rate limit for this service and returned no rate-limit header on any request, so this is courtesy
|
|
50
|
+
* pacing rather than a published ceiling — stated as such rather than dressed up as a measured limit. It costs an
|
|
51
|
+
* acquisition run nothing: a whole-state build makes one catalogue call, and the verification's per-point calls are
|
|
52
|
+
* measured at 1.8 s each anyway.
|
|
53
|
+
*/
|
|
54
|
+
export const SDA_MIN_REQUEST_INTERVAL_MS = 500;
|
|
55
|
+
/**
|
|
56
|
+
* How long a cached Soil Data Access response stays fresh.
|
|
57
|
+
*
|
|
58
|
+
* Twelve hours, chosen against the product's cadence rather than a wall-clock intuition: NRCS performs ONE coordinated
|
|
59
|
+
* Annual Soils Refresh, on October 1. Grouping `sacatalog` by year of `saverest` returns 2016: 1, 2025: 3,323, 2026: 56
|
|
60
|
+
* — 98.3% of survey areas carry a single version date from one refresh rather than a per-area drift. A shorter TTL buys
|
|
61
|
+
* nothing.
|
|
62
|
+
*/
|
|
63
|
+
const SDA_CACHE_TTL_MS = 12 * 60 * 60 * 1000;
|
|
64
|
+
/**
|
|
65
|
+
* The error a `ServiceExceptionReport` becomes.
|
|
66
|
+
*
|
|
67
|
+
* Its own class rather than a bare `Error`, because the three failures it carries need different responses from a
|
|
68
|
+
* caller: a timeout is worth narrowing the query for, an invalid column is a schema change, and access denied is a
|
|
69
|
+
* query the service will never run.
|
|
70
|
+
*/
|
|
71
|
+
export class SoilDataAccessError extends Error {
|
|
72
|
+
serviceException;
|
|
73
|
+
constructor(serviceException, query) {
|
|
74
|
+
super(`soil data access: the service returned a ServiceExceptionReport — ${serviceException} (query: ${query.slice(0, 200)})`);
|
|
75
|
+
this.name = "SoilDataAccessError";
|
|
76
|
+
this.serviceException = serviceException;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Did the service exceed its own query timeout? There is no published figure for it, so the message is the only
|
|
80
|
+
* signal — and it arrives on an HTTP 200.
|
|
81
|
+
*/
|
|
82
|
+
get timedOut() {
|
|
83
|
+
return /timed out/iu.test(this.serviceException);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The `<ServiceException>` text inside an OGC exception report, or `undefined` when the body is not one.
|
|
88
|
+
*
|
|
89
|
+
* Split from the request so the detection is testable against captured bodies. Both shapes below were taken from the
|
|
90
|
+
* live service: the report arrives with an XML declaration and an `xmlns` of `http://www.opengis.net/ogc`.
|
|
91
|
+
*/
|
|
92
|
+
export function readServiceException(body) {
|
|
93
|
+
if (!body.includes("ServiceExceptionReport"))
|
|
94
|
+
return undefined;
|
|
95
|
+
// A report whose exception element cannot be read is still a report, and reporting it as a successful empty answer
|
|
96
|
+
// is the failure this whole function exists to prevent.
|
|
97
|
+
return decodeXML((exceptionText(body) ?? "the report carried no readable ServiceException element").trim());
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The opening tag, without its terminator — the prefix `<ServiceExceptionReport …>` unhelpfully shares.
|
|
101
|
+
*/
|
|
102
|
+
const EXCEPTION_OPEN = "<ServiceException";
|
|
103
|
+
/**
|
|
104
|
+
* The inner text of the first real `<ServiceException>` element.
|
|
105
|
+
*
|
|
106
|
+
* INDEX SCANS RATHER THAN A REGEX. The obvious form — `/<ServiceException(?:\s[^>]*)?>([\s\S]*?)<\/ServiceException>/`
|
|
107
|
+
* — backtracks polynomially on a body whose opening tag has no closing partner, and this body is whatever a network
|
|
108
|
+
* service returned. Two more things it has to get right, both of which cost nothing here: the tag name must END at the
|
|
109
|
+
* match, because `<ServiceExceptionReport xmlns="…">` shares the prefix and taking it captures the entire report as the
|
|
110
|
+
* message; and an unclosed element reads as unreadable rather than as empty.
|
|
111
|
+
*/
|
|
112
|
+
function exceptionText(body) {
|
|
113
|
+
let cursor = 0;
|
|
114
|
+
for (;;) {
|
|
115
|
+
const start = body.indexOf(EXCEPTION_OPEN, cursor);
|
|
116
|
+
if (start === -1)
|
|
117
|
+
return undefined;
|
|
118
|
+
const after = start + EXCEPTION_OPEN.length;
|
|
119
|
+
cursor = after;
|
|
120
|
+
// `>` closes a bare tag; whitespace introduces attributes. Anything else continues the tag NAME, which means this
|
|
121
|
+
// is `ServiceExceptionReport` or a sibling and not the element being read.
|
|
122
|
+
if (!/^[\s>]/u.test(body.slice(after, after + 1)))
|
|
123
|
+
continue;
|
|
124
|
+
const contentStart = body.indexOf(">", after);
|
|
125
|
+
if (contentStart === -1)
|
|
126
|
+
return undefined;
|
|
127
|
+
const end = body.indexOf("</ServiceException>", contentStart);
|
|
128
|
+
if (end === -1)
|
|
129
|
+
return undefined;
|
|
130
|
+
return body.slice(contentStart + 1, end);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* A client for Soil Data Access.
|
|
135
|
+
*/
|
|
136
|
+
export class SoilDataAccessClient extends APIClient {
|
|
137
|
+
/**
|
|
138
|
+
* Run one query and return its rows.
|
|
139
|
+
*
|
|
140
|
+
* @throws {SoilDataAccessError} When the service answers with an exception report — including on an HTTP 200, which
|
|
141
|
+
* is what a server-side timeout does.
|
|
142
|
+
*/
|
|
143
|
+
async query(sql) {
|
|
144
|
+
const { data } = await this.fetch({
|
|
145
|
+
method: "POST",
|
|
146
|
+
url: SDA_POST_REST_URL,
|
|
147
|
+
// TEXT, not JSON, and that is the whole trap. A JSON response type hands a failure body to a JSON parser,
|
|
148
|
+
// which either throws something unrelated to what went wrong or — on a 200 — yields nothing at all.
|
|
149
|
+
responseType: "text",
|
|
150
|
+
headers: { "Content-Type": "application/json" },
|
|
151
|
+
data: { SERVICE: "query", FORMAT: "JSON", QUERY: sql },
|
|
152
|
+
});
|
|
153
|
+
const exception = readServiceException(data);
|
|
154
|
+
if (exception)
|
|
155
|
+
throw new SoilDataAccessError(exception, sql);
|
|
156
|
+
const parsed = parseJSONStrict(data);
|
|
157
|
+
// An answer with NO rows is `{}` rather than `{"Table":[]}`, so an absent `Table` is a real empty result and not a
|
|
158
|
+
// read failure — the exception check above has already separated the two.
|
|
159
|
+
if (parsed.Table === undefined)
|
|
160
|
+
return [];
|
|
161
|
+
if (!Array.isArray(parsed.Table)) {
|
|
162
|
+
throw new TypeError(`soil data access: the service answered with a Table that is not an array (${typeof parsed.Table}) — the response format changed`);
|
|
163
|
+
}
|
|
164
|
+
return parsed.Table.map((row) => row.map((value) => (value === null ? "" : String(value))));
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The published survey areas whose symbol starts with `prefix` — a state code for a state-scoped build, or a whole
|
|
168
|
+
* symbol for the single-area rung.
|
|
169
|
+
*
|
|
170
|
+
* @throws {Error} When the catalogue returns nothing. An empty catalogue for a prefix a caller named is either a typo
|
|
171
|
+
* or a service change, and building zero survey areas while reporting success is the shape this refuses.
|
|
172
|
+
*/
|
|
173
|
+
async readSurveyAreaCatalog(prefix) {
|
|
174
|
+
const escaped = prefix.replaceAll("'", "''");
|
|
175
|
+
const rows = await this.query(`SELECT areasymbol, areaname, saverest, saversion FROM sacatalog WHERE areasymbol LIKE '${escaped}%' ORDER BY areasymbol`);
|
|
176
|
+
if (!rows.length) {
|
|
177
|
+
throw new Error(`soil data access: the catalogue holds no survey area whose symbol starts with ${JSON.stringify(prefix)} — a build over an empty set would report success having written nothing`);
|
|
178
|
+
}
|
|
179
|
+
return rows.map((row) => ({
|
|
180
|
+
areasymbol: row[0],
|
|
181
|
+
areaname: row[1],
|
|
182
|
+
saverest: saverestToISODate(row[2]),
|
|
183
|
+
saversion: Number(row[3]),
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Which map unit the service's OWN geometry assigns at a point, or `undefined` where it assigns none.
|
|
188
|
+
*
|
|
189
|
+
* This is the second path the built artifact is checked against: same authority, different distribution channel, and
|
|
190
|
+
* geometry this package has never touched. Measured at 1.807 s per point, so a few hundred points is minutes.
|
|
191
|
+
*/
|
|
192
|
+
async mukeyAtPoint(latitude, longitude) {
|
|
193
|
+
const rows = await this.query(`SELECT mukey FROM SDA_Get_Mukey_from_intersection_with_WktWgs84('point(${longitude} ${latitude})')`);
|
|
194
|
+
return rows[0]?.[0] || undefined;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Build a {@link SoilDataAccessClient} with the disk cache and pacing this package's acquisition path expects.
|
|
199
|
+
*/
|
|
200
|
+
export function createSoilDataAccessClient(options = {}) {
|
|
201
|
+
return new SoilDataAccessClient({
|
|
202
|
+
displayName: "SoilDataAccess",
|
|
203
|
+
minRequestIntervalMs: options.minRequestIntervalMs ?? SDA_MIN_REQUEST_INTERVAL_MS,
|
|
204
|
+
retry: true,
|
|
205
|
+
...(options.clock ? { clock: options.clock } : {}),
|
|
206
|
+
caching: {
|
|
207
|
+
ttl: SDA_CACHE_TTL_MS,
|
|
208
|
+
storage: buildDiskStorage({
|
|
209
|
+
directory: options.cacheDirectory ?? String(dataRootPath("soil", "cache", "http")),
|
|
210
|
+
}),
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../lib/sdk/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Acquire one survey area's published archive — 13 to 41 MB streamed to disk and unzipped.
|
|
7
|
+
*
|
|
8
|
+
* THE TRANSFER ITSELF LIVES IN `@mailwoman/core/utils`, and `streamToDisk` carries why a file transfer of
|
|
9
|
+
* this size keeps raw `fetch` instead of going through `APIClient`, plus the `.part`-rename rule. What is
|
|
10
|
+
* soil's, and stays here, is the URL shape, the cache key, and the two facts below that the shared
|
|
11
|
+
* transfer is told rather than assumes: the progress stride and what a 400 means. The METADATA reads
|
|
12
|
+
* around this one do go through `APIClient` — see `client.ts`.
|
|
13
|
+
*
|
|
14
|
+
* FRESHNESS IS `sacatalog.saverest`, NEVER A LENGTH PROBE, AND THE HOST LEAVES NO CHOICE. It answers `HEAD`
|
|
15
|
+
* with HTTP 405 (`allow: GET`) and IGNORES `Range`: a request with `Range: bytes=0-0` returned HTTP 200 and
|
|
16
|
+
* transferred the whole 27,598,377 bytes in 7.23 s. So "check the size first" starts a real download. The
|
|
17
|
+
* cache is keyed on the version date the tabular service reports instead, and a vintage already on disk is
|
|
18
|
+
* never re-fetched. The `Range` behaviour is PATH-SPECIFIC rather than host-wide — `/DataAvailability/`
|
|
19
|
+
* does answer 206 — so a client must probe per path rather than conclude from one.
|
|
20
|
+
*
|
|
21
|
+
* THE FILENAME EMBEDS THE VERSION DATE AND A WRONG ONE IS AN HTTP 400. Not a 404: asking for a date the
|
|
22
|
+
* host does not hold reads as a malformed request rather than a missing file, which is why the date comes
|
|
23
|
+
* from the catalogue rather than from a guess. The square brackets must be sent literally, so the URL is
|
|
24
|
+
* built with them percent-encoded.
|
|
25
|
+
*
|
|
26
|
+
* TWO CACHE VARIANTS EXIST AND THE BARE ONE IS WANTED. `wss_SSA_IA153_[2025-09-09].zip` is 25,474,922 bytes;
|
|
27
|
+
* `wss_SSA_IA153_soildb_IA_2003_[2025-09-09].zip` is 27,598,377 and differs only by an EMPTY Microsoft Access
|
|
28
|
+
* template container for a workflow this program does not use. Confirmed on a second area (`IA015`:
|
|
29
|
+
* 38,981,269 against 41,104,724 bytes) and on a third that ships no template at all (`TX299`, 13,455,641
|
|
30
|
+
* bytes, 97 files, no `.mdb`).
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* The download service's survey-area cache. Documented at `https://websoilsurvey.sc.egov.usda.gov/DSD/Download/help`,
|
|
34
|
+
* which lists `GET /{CacheName}/{FileName}`.
|
|
35
|
+
*/
|
|
36
|
+
export declare const WSS_SSA_CACHE_URL = "https://websoilsurvey.sc.egov.usda.gov/DSD/Download/Cache/SSA";
|
|
37
|
+
/**
|
|
38
|
+
* The archive URL for one survey area at one version date.
|
|
39
|
+
*
|
|
40
|
+
* The brackets are percent-encoded rather than sent raw: they are not valid in a URL path, and a client that sends them
|
|
41
|
+
* literally depends on the fetcher tolerating them.
|
|
42
|
+
*/
|
|
43
|
+
export declare function surveyAreaArchiveURL(areaSymbol: string, versionDate: string): string;
|
|
44
|
+
export interface DownloadSurveyAreaOptions {
|
|
45
|
+
areaSymbol: string;
|
|
46
|
+
/**
|
|
47
|
+
* The version date from `sacatalog.saverest`, as `YYYY-MM-DD`.
|
|
48
|
+
*/
|
|
49
|
+
versionDate: string;
|
|
50
|
+
/**
|
|
51
|
+
* Where vintages are kept. Each version date gets its own directory, so a new refresh never overwrites the old one in
|
|
52
|
+
* place and a re-run against the same vintage never re-transfers.
|
|
53
|
+
*/
|
|
54
|
+
cacheRoot: string;
|
|
55
|
+
onProgress?: (message: string) => void;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* What one acquired survey area is, on disk.
|
|
59
|
+
*/
|
|
60
|
+
export interface SurveyAreaArchive {
|
|
61
|
+
areaSymbol: string;
|
|
62
|
+
versionDate: string;
|
|
63
|
+
/**
|
|
64
|
+
* The extracted `<areasymbol>/` directory, holding `spatial/` and `tabular/`.
|
|
65
|
+
*/
|
|
66
|
+
root: string;
|
|
67
|
+
spatialDirectory: string;
|
|
68
|
+
tabularDirectory: string;
|
|
69
|
+
/**
|
|
70
|
+
* The archive as transferred. Kept so a re-run costs nothing and so the bytes are re-checkable.
|
|
71
|
+
*/
|
|
72
|
+
archivePath: string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Download and unzip one survey area, returning where its pieces landed.
|
|
76
|
+
*
|
|
77
|
+
* Downloads to a `.part` file and renames only on a clean finish, so an interrupted transfer never presents as a
|
|
78
|
+
* complete archive — the same discipline the database build uses, for the same reason.
|
|
79
|
+
*
|
|
80
|
+
* @throws {Error} When the host answers anything but 200, or when the extracted tree does not hold the two directories
|
|
81
|
+
* every survey area publishes.
|
|
82
|
+
*/
|
|
83
|
+
export declare function downloadSurveyArea(options: DownloadSurveyAreaOptions): Promise<SurveyAreaArchive>;
|
|
84
|
+
//# sourceMappingURL=download.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../lib/sdk/download.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAQH;;;GAGG;AACH,eAAO,MAAM,iBAAiB,kEAAkE,CAAA;AAEhG;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAEpF;AAED,MAAM,WAAW,yBAAyB;IACzC,UAAU,EAAE,MAAM,CAAA;IAClB;;OAEG;IACH,WAAW,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACtC;AAcD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IACjC,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB;;OAEG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ,gBAAgB,EAAE,MAAM,CAAA;IACxB,gBAAgB,EAAE,MAAM,CAAA;IACxB;;OAEG;IACH,WAAW,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAqDvG"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Acquire one survey area's published archive — 13 to 41 MB streamed to disk and unzipped.
|
|
7
|
+
*
|
|
8
|
+
* THE TRANSFER ITSELF LIVES IN `@mailwoman/core/utils`, and `streamToDisk` carries why a file transfer of
|
|
9
|
+
* this size keeps raw `fetch` instead of going through `APIClient`, plus the `.part`-rename rule. What is
|
|
10
|
+
* soil's, and stays here, is the URL shape, the cache key, and the two facts below that the shared
|
|
11
|
+
* transfer is told rather than assumes: the progress stride and what a 400 means. The METADATA reads
|
|
12
|
+
* around this one do go through `APIClient` — see `client.ts`.
|
|
13
|
+
*
|
|
14
|
+
* FRESHNESS IS `sacatalog.saverest`, NEVER A LENGTH PROBE, AND THE HOST LEAVES NO CHOICE. It answers `HEAD`
|
|
15
|
+
* with HTTP 405 (`allow: GET`) and IGNORES `Range`: a request with `Range: bytes=0-0` returned HTTP 200 and
|
|
16
|
+
* transferred the whole 27,598,377 bytes in 7.23 s. So "check the size first" starts a real download. The
|
|
17
|
+
* cache is keyed on the version date the tabular service reports instead, and a vintage already on disk is
|
|
18
|
+
* never re-fetched. The `Range` behaviour is PATH-SPECIFIC rather than host-wide — `/DataAvailability/`
|
|
19
|
+
* does answer 206 — so a client must probe per path rather than conclude from one.
|
|
20
|
+
*
|
|
21
|
+
* THE FILENAME EMBEDS THE VERSION DATE AND A WRONG ONE IS AN HTTP 400. Not a 404: asking for a date the
|
|
22
|
+
* host does not hold reads as a malformed request rather than a missing file, which is why the date comes
|
|
23
|
+
* from the catalogue rather than from a guess. The square brackets must be sent literally, so the URL is
|
|
24
|
+
* built with them percent-encoded.
|
|
25
|
+
*
|
|
26
|
+
* TWO CACHE VARIANTS EXIST AND THE BARE ONE IS WANTED. `wss_SSA_IA153_[2025-09-09].zip` is 25,474,922 bytes;
|
|
27
|
+
* `wss_SSA_IA153_soildb_IA_2003_[2025-09-09].zip` is 27,598,377 and differs only by an EMPTY Microsoft Access
|
|
28
|
+
* template container for a workflow this program does not use. Confirmed on a second area (`IA015`:
|
|
29
|
+
* 38,981,269 against 41,104,724 bytes) and on a third that ships no template at all (`TX299`, 13,455,641
|
|
30
|
+
* bytes, 97 files, no `.mdb`).
|
|
31
|
+
*/
|
|
32
|
+
import { tryStat } from "@mailwoman/core/fs/readers";
|
|
33
|
+
import { makeDirectories } from "@mailwoman/core/fs/writers";
|
|
34
|
+
import { runFile } from "@mailwoman/core/process";
|
|
35
|
+
import { streamToDisk } from "@mailwoman/core/utils";
|
|
36
|
+
import { join } from "path-ts";
|
|
37
|
+
/**
|
|
38
|
+
* The download service's survey-area cache. Documented at `https://websoilsurvey.sc.egov.usda.gov/DSD/Download/help`,
|
|
39
|
+
* which lists `GET /{CacheName}/{FileName}`.
|
|
40
|
+
*/
|
|
41
|
+
export const WSS_SSA_CACHE_URL = "https://websoilsurvey.sc.egov.usda.gov/DSD/Download/Cache/SSA";
|
|
42
|
+
/**
|
|
43
|
+
* The archive URL for one survey area at one version date.
|
|
44
|
+
*
|
|
45
|
+
* The brackets are percent-encoded rather than sent raw: they are not valid in a URL path, and a client that sends them
|
|
46
|
+
* literally depends on the fetcher tolerating them.
|
|
47
|
+
*/
|
|
48
|
+
export function surveyAreaArchiveURL(areaSymbol, versionDate) {
|
|
49
|
+
return `${WSS_SSA_CACHE_URL}/wss_SSA_${areaSymbol}_%5B${versionDate}%5D.zip`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Bytes between progress reports. Smaller than the shared default because these archives are 13–41 MB, and the default
|
|
53
|
+
* stride would leave the smallest of them reporting once.
|
|
54
|
+
*/
|
|
55
|
+
const PROGRESS_STRIDE_BYTES = 8 * 1024 * 1024;
|
|
56
|
+
/**
|
|
57
|
+
* What this host answers for a version date it does not hold. NOT a 404: it reads as a malformed request rather than a
|
|
58
|
+
* missing file, which is why the message below says so and why the date comes from the catalogue rather than a guess.
|
|
59
|
+
*/
|
|
60
|
+
const UNKNOWN_VERSION_STATUS = 400;
|
|
61
|
+
/**
|
|
62
|
+
* Download and unzip one survey area, returning where its pieces landed.
|
|
63
|
+
*
|
|
64
|
+
* Downloads to a `.part` file and renames only on a clean finish, so an interrupted transfer never presents as a
|
|
65
|
+
* complete archive — the same discipline the database build uses, for the same reason.
|
|
66
|
+
*
|
|
67
|
+
* @throws {Error} When the host answers anything but 200, or when the extracted tree does not hold the two directories
|
|
68
|
+
* every survey area publishes.
|
|
69
|
+
*/
|
|
70
|
+
export async function downloadSurveyArea(options) {
|
|
71
|
+
const vintageDirectory = join(options.cacheRoot, options.versionDate);
|
|
72
|
+
const root = join(vintageDirectory, options.areaSymbol);
|
|
73
|
+
const archivePath = join(vintageDirectory, `wss_SSA_${options.areaSymbol}.zip`);
|
|
74
|
+
if (!(await tryStat(root))) {
|
|
75
|
+
await makeDirectories(vintageDirectory);
|
|
76
|
+
if (await tryStat(archivePath)) {
|
|
77
|
+
options.onProgress?.(`${options.areaSymbol}: archive for ${options.versionDate} already downloaded`);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
await streamToDisk({
|
|
81
|
+
url: surveyAreaArchiveURL(options.areaSymbol, options.versionDate),
|
|
82
|
+
destination: archivePath,
|
|
83
|
+
context: "soil download",
|
|
84
|
+
progressStrideBytes: PROGRESS_STRIDE_BYTES,
|
|
85
|
+
describeStatus: (status) => status === UNKNOWN_VERSION_STATUS
|
|
86
|
+
? " — this host answers 400 rather than 404 for a version date it does not hold, so check the date against sacatalog.saverest"
|
|
87
|
+
: undefined,
|
|
88
|
+
// Every progress line names the area, because a full acquisition interleaves hundreds of them.
|
|
89
|
+
...(options.onProgress
|
|
90
|
+
? { onProgress: (message) => options.onProgress?.(`${options.areaSymbol}: ${message}`) }
|
|
91
|
+
: {}),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
// The archive holds its files under an `<AREASYMBOL>/` root already, so it unzips into the vintage directory
|
|
95
|
+
// rather than into a directory named for itself.
|
|
96
|
+
await runFile("unzip", ["-o", "-q", archivePath, "-d", vintageDirectory]);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
options.onProgress?.(`${options.areaSymbol}: already extracted for ${options.versionDate}`);
|
|
100
|
+
}
|
|
101
|
+
const spatialDirectory = join(root, "spatial");
|
|
102
|
+
const tabularDirectory = join(root, "tabular");
|
|
103
|
+
for (const directory of [spatialDirectory, tabularDirectory]) {
|
|
104
|
+
if (!(await tryStat(directory))) {
|
|
105
|
+
throw new Error(`soil download: ${options.areaSymbol} extracted without a ${directory} directory — every survey area publishes both spatial/ and tabular/, so this archive is not the product`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
areaSymbol: options.areaSymbol,
|
|
110
|
+
versionDate: options.versionDate,
|
|
111
|
+
root,
|
|
112
|
+
spatialDirectory,
|
|
113
|
+
tabularDirectory,
|
|
114
|
+
archivePath,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=download.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"download.js","sourceRoot":"","sources":["../../lib/sdk/download.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAA;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAC5D,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAA;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACpD,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAE9B;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,+DAA+D,CAAA;AAEhG;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,UAAkB,EAAE,WAAmB;IAC3E,OAAO,GAAG,iBAAiB,YAAY,UAAU,OAAO,WAAW,SAAS,CAAA;AAC7E,CAAC;AAgBD;;;GAGG;AACH,MAAM,qBAAqB,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAA;AAE7C;;;GAGG;AACH,MAAM,sBAAsB,GAAG,GAAG,CAAA;AAoBlC;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,OAAkC;IAC1E,MAAM,gBAAgB,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,WAAW,CAAC,CAAA;IACrE,MAAM,IAAI,GAAG,IAAI,CAAC,gBAAgB,EAAE,OAAO,CAAC,UAAU,CAAC,CAAA;IACvD,MAAM,WAAW,GAAG,IAAI,CAAC,gBAAgB,EAAE,WAAW,OAAO,CAAC,UAAU,MAAM,CAAC,CAAA;IAE/E,IAAI,CAAC,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;QAC5B,MAAM,eAAe,CAAC,gBAAgB,CAAC,CAAA;QAEvC,IAAI,MAAM,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;YAChC,OAAO,CAAC,UAAU,EAAE,CAAC,GAAG,OAAO,CAAC,UAAU,iBAAiB,OAAO,CAAC,WAAW,qBAAqB,CAAC,CAAA;QACrG,CAAC;aAAM,CAAC;YACP,MAAM,YAAY,CAAC;gBAClB,GAAG,EAAE,oBAAoB,CAAC,OAAO,CAAC,UAAU,EAAE,OAAO,CAAC,WAAW,CAAC;gBAClE,WAAW,EAAE,WAAW;gBACxB,OAAO,EAAE,eAAe;gBACxB,mBAAmB,EAAE,qBAAqB;gBAC1C,cAAc,EAAE,CAAC,MAAM,EAAE,EAAE,CAC1B,MAAM,KAAK,sBAAsB;oBAChC,CAAC,CAAC,4HAA4H;oBAC9H,CAAC,CAAC,SAAS;gBACb,+FAA+F;gBAC/F,GAAG,CAAC,OAAO,CAAC,UAAU;oBACrB,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,OAAe,EAAE,EAAE,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,GAAG,OAAO,CAAC,UAAU,KAAK,OAAO,EAAE,CAAC,EAAE;oBAChG,CAAC,CAAC,EAAE,CAAC;aACN,CAAC,CAAA;QACH,CAAC;QAED,6GAA6G;QAC7G,iDAAiD;QACjD,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,gBAAgB,CAAC,CAAC,CAAA;IAC1E,CAAC;SAAM,CAAC;QACP,OAAO,CAAC,UAAU,EAAE,CAAC,GAAG,OAAO,CAAC,UAAU,2BAA2B,OAAO,CAAC,WAAW,EAAE,CAAC,CAAA;IAC5F,CAAC;IAED,MAAM,gBAAgB,GAAG,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAA;IAC9C,MAAM,gBAAgB,GAAG,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAA;IAE9C,KAAK,MAAM,SAAS,IAAI,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,EAAE,CAAC;QAC9D,IAAI,CAAC,CAAC,MAAM,OAAO,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC;YACjC,MAAM,IAAI,KAAK,CACd,kBAAkB,OAAO,CAAC,UAAU,wBAAwB,SAAS,yGAAyG,CAC9K,CAAA;QACF,CAAC;IACF,CAAC;IAED,OAAO;QACN,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,IAAI;QACJ,gBAAgB;QAChB,gBAAgB;QAChB,WAAW;KACX,CAAA;AACF,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
* @file The acquisition + build surface for the NRCS SSURGO soil-capability layer. The READER is the
|
|
6
|
+
* package root.
|
|
7
|
+
*/
|
|
8
|
+
export * from "#sdk/acquire";
|
|
9
|
+
export * from "#sdk/build-soil";
|
|
10
|
+
export * from "#sdk/cell-tiers";
|
|
11
|
+
export * from "#sdk/cells";
|
|
12
|
+
export * from "#sdk/client";
|
|
13
|
+
export * from "#sdk/download";
|
|
14
|
+
export * from "#sdk/ingest";
|
|
15
|
+
export * from "#sdk/measure-resolutions";
|
|
16
|
+
export * from "#sdk/reduce";
|
|
17
|
+
export * from "#sdk/survey-area";
|
|
18
|
+
export * from "#sdk/tabular";
|
|
19
|
+
export * from "#sdk/verify";
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../lib/sdk/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA;AAC3B,cAAc,0BAA0B,CAAA;AACxC,cAAc,aAAa,CAAA;AAC3B,cAAc,kBAAkB,CAAA;AAChC,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA"}
|
package/out/sdk/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
* @file The acquisition + build surface for the NRCS SSURGO soil-capability layer. The READER is the
|
|
6
|
+
* package root.
|
|
7
|
+
*/
|
|
8
|
+
export * from "#sdk/acquire";
|
|
9
|
+
export * from "#sdk/build-soil";
|
|
10
|
+
export * from "#sdk/cell-tiers";
|
|
11
|
+
export * from "#sdk/cells";
|
|
12
|
+
export * from "#sdk/client";
|
|
13
|
+
export * from "#sdk/download";
|
|
14
|
+
export * from "#sdk/ingest";
|
|
15
|
+
export * from "#sdk/measure-resolutions";
|
|
16
|
+
export * from "#sdk/reduce";
|
|
17
|
+
export * from "#sdk/survey-area";
|
|
18
|
+
export * from "#sdk/tabular";
|
|
19
|
+
export * from "#sdk/verify";
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../lib/sdk/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA;AAC3B,cAAc,0BAA0B,CAAA;AACxC,cAAc,aAAa,CAAA;AAC3B,cAAc,kBAAkB,CAAA;AAChC,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* The streaming pass — every delineation into `soil_map_unit_area` and into the build's touch table — as a
|
|
7
|
+
* unit of work that can run over PART of one survey area.
|
|
8
|
+
*
|
|
9
|
+
* WHY THIS IS A CHUNK RATHER THAN A WHOLE FILE. h3's WASM heap cannot be reset from JavaScript, and it does
|
|
10
|
+
* not survive an unbounded number of polyfill calls: over the flood layer's real product, runs died after
|
|
11
|
+
* roughly 510,000 and 798,000 features on geometry that classifies in milliseconds in a fresh process. A
|
|
12
|
+
* build that completes only when fragmentation happens to stay low is not a reproducible build, so the
|
|
13
|
+
* classification is bounded by construction — {@linkcode buildSoilDatabase} runs one of these per range of
|
|
14
|
+
* the shapefile's own FIDs, each in its own process, and each therefore against a heap that starts empty.
|
|
15
|
+
* Iowa's 99 survey areas hold far more delineations together than any one of them does, which is why the
|
|
16
|
+
* bound is per RANGE rather than per area.
|
|
17
|
+
*
|
|
18
|
+
* THE CHUNK OWNS NO ARTIFACT. It appends rows to a database the parent created and will seal, and returns
|
|
19
|
+
* counts the parent adds up. Chunks run one at a time against that file, so there is no concurrent writer
|
|
20
|
+
* and no locking to reason about.
|
|
21
|
+
*/
|
|
22
|
+
import type { DatabaseClient } from "@mailwoman/sqlite/client";
|
|
23
|
+
import type { SoilDatabase } from "#schema";
|
|
24
|
+
import type { SoilFeatureSource } from "#sdk/ingest";
|
|
25
|
+
/**
|
|
26
|
+
* What one chunk produced. Every field is JSON-serializable, because a chunk normally reports across a process
|
|
27
|
+
* boundary.
|
|
28
|
+
*/
|
|
29
|
+
export interface SoilChunkResult {
|
|
30
|
+
areaSymbol: string;
|
|
31
|
+
delineations: number;
|
|
32
|
+
/**
|
|
33
|
+
* Delineations whose bounding box forced a resolution coarser than the target.
|
|
34
|
+
*/
|
|
35
|
+
coarsened: number;
|
|
36
|
+
/**
|
|
37
|
+
* `[coverageCell, delineationsReachingIt]` pairs — an array rather than a `Map` so it survives the process boundary.
|
|
38
|
+
*/
|
|
39
|
+
observedByCoverageCell: Array<[number, number]>;
|
|
40
|
+
/**
|
|
41
|
+
* The same, counting only delineations whose map unit HAS soil mapping behind it.
|
|
42
|
+
*
|
|
43
|
+
* Separate from the total because the coverage rule turns on it: a coverage cell reached only by `NOTCOM` and
|
|
44
|
+
* access-denied polygons is inside a published survey area and carries no digitized soil mapping, and the survey's
|
|
45
|
+
* §3.2 gives it no row.
|
|
46
|
+
*/
|
|
47
|
+
mappedByCoverageCell: Array<[number, number]>;
|
|
48
|
+
/**
|
|
49
|
+
* Square metres: the encoded rings read WITH their holes, and read without.
|
|
50
|
+
*/
|
|
51
|
+
area: {
|
|
52
|
+
nestedM2: number;
|
|
53
|
+
allExteriorM2: number;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export interface IngestSoilChunkOptions {
|
|
57
|
+
source: SoilFeatureSource;
|
|
58
|
+
indexResolution: number;
|
|
59
|
+
coverageResolution: number;
|
|
60
|
+
/**
|
|
61
|
+
* The map units with NO soil mapping behind them — `NOTCOM`, `NOTPUB`, access denied, or no readable component
|
|
62
|
+
* weights. Passed in rather than joined here so the chunk stays a streaming pass over geometry.
|
|
63
|
+
*/
|
|
64
|
+
noMappingMukeys: ReadonlySet<string>;
|
|
65
|
+
onProgress?: (message: string) => void;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Stream one chunk of one survey area into `database`.
|
|
69
|
+
*
|
|
70
|
+
* @throws {Error} On a delineation the classifier refuses — which includes the allocator's silent zero-cell answer.
|
|
71
|
+
*/
|
|
72
|
+
export declare function ingestSoilChunk(database: DatabaseClient<SoilDatabase>, options: IngestSoilChunkOptions): Promise<SoilChunkResult>;
|
|
73
|
+
//# sourceMappingURL=ingest-chunk.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ingest-chunk.d.ts","sourceRoot":"","sources":["../../lib/sdk/ingest-chunk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AAE9D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAE3C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAapD;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC/B,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB;;OAEG;IACH,sBAAsB,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAC/C;;;;;;OAMG;IACH,oBAAoB,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAC7C;;OAEG;IACH,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAA;CACjD;AAED,MAAM,WAAW,sBAAsB;IACtC,MAAM,EAAE,iBAAiB,CAAA;IACzB,eAAe,EAAE,MAAM,CAAA;IACvB,kBAAkB,EAAE,MAAM,CAAA;IAC1B;;;OAGG;IACH,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IACpC,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACtC;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACpC,QAAQ,EAAE,cAAc,CAAC,YAAY,CAAC,EACtC,OAAO,EAAE,sBAAsB,GAC7B,OAAO,CAAC,eAAe,CAAC,CA2F1B"}
|