@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,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Read a survey area's published shapefiles as a stream of WGS84 delineations, through ogr2ogr.
|
|
7
|
+
*
|
|
8
|
+
* OGR IS BUILD TOOLING, NEVER A SERVE DEPENDENCY (SCOPE invariant 6). It converts the authority's geometry
|
|
9
|
+
* into the structure the runtime probes, and nothing downstream of this module knows GDAL exists.
|
|
10
|
+
*
|
|
11
|
+
* THE SOURCE IS ALREADY IN WGS84, AND CHECKING IT IS STILL THE CHECK. Each `.prj` is an ESRI WKT reading
|
|
12
|
+
* `GEOGCS["GCS_WGS_1984",…]`, which GDAL resolves to EPSG:4326 — so no reprojection is needed before H3.
|
|
13
|
+
* The authority code is asserted anyway before a single feature is read, and the reprojected stream is
|
|
14
|
+
* asserted against the layer's own declared extent, which is the check that catches a coordinate-order
|
|
15
|
+
* mistake the projection check cannot see.
|
|
16
|
+
*
|
|
17
|
+
* THE DATUM GUARD RUNS EVEN THOUGH THE ANSWER IS THE IDENTITY, AND THAT IS THE POINT. PROJ substitutes a
|
|
18
|
+
* ballpark datum shift SILENTLY when the accurate grid is missing — measured on the flood layer at 3.4 m
|
|
19
|
+
* over an entire country, visible only as eight disagreements out of 59 against the authority's own
|
|
20
|
+
* service. For an EPSG:4326 source `projinfo` answers `Null geographic offset from WGS 84 to WGS 84, 0 m,
|
|
21
|
+
* World.` and the guard passes in one process. Skipping it on the reasoning that this source needs no
|
|
22
|
+
* shift is how the guard comes to be missing on the day a source arrives that does.
|
|
23
|
+
*
|
|
24
|
+
* THE ID IS THE SHAPEFILE'S OWN FID, AND IT HAS TO BE, because SSURGO publishes no per-delineation key:
|
|
25
|
+
* `MUKEY` names the MAP UNIT and one map unit has many delineations — `IA153` holds 17,966 delineations
|
|
26
|
+
* across 152 map units. So `area_id` is `<areasymbol>:<fid>`, which is stable across runs and is what makes
|
|
27
|
+
* a bounded chunk name the same features every time.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { assertRingsInsideExtent, requireArealPolygons, type MultiPolygonRings } from "@mailwoman/spatial"
|
|
31
|
+
import { readOGRLayerIdentity } from "@mailwoman/spatial/tools/ogr"
|
|
32
|
+
import { ogr2ogrGeoJSONSeq } from "@mailwoman/spatial/tools/ogr-stream"
|
|
33
|
+
import { basename, join } from "path-ts"
|
|
34
|
+
|
|
35
|
+
import { SSURGO_SOURCE_EPSG } from "#vocabulary"
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Coordinate decimals ogr2ogr writes into the stream. Nine is well past the source's own precision — the metadata
|
|
39
|
+
* states compilation to base maps meeting National Map Accuracy Standards at 1 inch = 1,000 feet — and is chosen so the
|
|
40
|
+
* round trip contributes nothing measurable to the area cross-check.
|
|
41
|
+
*/
|
|
42
|
+
const COORDINATE_PRECISION = 9
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* How far outside the layer's declared extent a vertex may fall before the ingest refuses.
|
|
46
|
+
*
|
|
47
|
+
* A tenth of a degree is about 11 km — small enough that an unprojected or axis-swapped read, which lands whole
|
|
48
|
+
* hemispheres away, still fails, and loose enough that a rounded declared extent is not brittle.
|
|
49
|
+
*/
|
|
50
|
+
const BBOX_MARGIN_DEGREES = 0.1
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The shapefile holding a survey area's map-unit polygons — the delineations this layer stores.
|
|
54
|
+
*/
|
|
55
|
+
export function mapUnitShapefile(spatialDirectory: string, areaSymbol: string): string {
|
|
56
|
+
return join(spatialDirectory, `soilmu_a_${areaSymbol.toLowerCase()}.shp`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The shapefile holding a survey area's own OUTLINE. The footprint comes from HERE and never from the union of the
|
|
61
|
+
* rated polygons — `NOTCOM` and access-denied map units are inside the footprint and carry no rating, so a footprint
|
|
62
|
+
* derived from the rated set would report them as unmapped when the authority has declared exactly what they are.
|
|
63
|
+
*/
|
|
64
|
+
export function surveyAreaShapefile(spatialDirectory: string, areaSymbol: string): string {
|
|
65
|
+
return join(spatialDirectory, `soilsa_a_${areaSymbol.toLowerCase()}.shp`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* One map-unit delineation, reprojected to WGS84.
|
|
70
|
+
*/
|
|
71
|
+
export interface SoilDelineation {
|
|
72
|
+
/**
|
|
73
|
+
* `<areasymbol>:<fid>`.
|
|
74
|
+
*/
|
|
75
|
+
areaID: string
|
|
76
|
+
mukey: string
|
|
77
|
+
areasymbol: string
|
|
78
|
+
polygons: MultiPolygonRings
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* What a shapefile says about itself, read before any feature is.
|
|
83
|
+
*/
|
|
84
|
+
export interface SoilSourceIdentity {
|
|
85
|
+
epsg: number
|
|
86
|
+
featureCount: number
|
|
87
|
+
layer: string
|
|
88
|
+
/**
|
|
89
|
+
* The layer's own declared extent, `[minLon, minLat, maxLon, maxLat]`.
|
|
90
|
+
*/
|
|
91
|
+
bbox: readonly [number, number, number, number]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface SoilIngestOptions {
|
|
95
|
+
shapefilePath: string
|
|
96
|
+
/**
|
|
97
|
+
* Layer inside it. Defaults to the shapefile's base name, which is what the ESRI driver reports.
|
|
98
|
+
*/
|
|
99
|
+
layer?: string
|
|
100
|
+
/**
|
|
101
|
+
* The EPSG code the source must declare.
|
|
102
|
+
*/
|
|
103
|
+
expectEPSG?: number
|
|
104
|
+
/**
|
|
105
|
+
* Read only the shapefile's own FIDs in `[fidFrom, fidTo]`, inclusive — what makes a bounded chunk possible.
|
|
106
|
+
*/
|
|
107
|
+
fidFrom?: number
|
|
108
|
+
fidTo?: number
|
|
109
|
+
/**
|
|
110
|
+
* Stop after this many features. The fixture and smoke rungs use it; a full build does not set it.
|
|
111
|
+
*/
|
|
112
|
+
limit?: number
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Read what the shapefile declares about itself, and refuse a projection this ingest was not written for.
|
|
117
|
+
*
|
|
118
|
+
* @throws {Error} When the layer is missing, declares no EPSG authority code, declares one other than `expectEPSG`, or
|
|
119
|
+
* reports no feature count.
|
|
120
|
+
*/
|
|
121
|
+
export async function readSoilSourceIdentity(options: SoilIngestOptions): Promise<SoilSourceIdentity> {
|
|
122
|
+
const identity = await readOGRLayerIdentity({
|
|
123
|
+
path: options.shapefilePath,
|
|
124
|
+
layer: options.layer ?? basename(options.shapefilePath, ".shp"),
|
|
125
|
+
expectEPSG: options.expectEPSG ?? SSURGO_SOURCE_EPSG,
|
|
126
|
+
context: "soil ingest",
|
|
127
|
+
requireExtent: true,
|
|
128
|
+
messages: {
|
|
129
|
+
noAuthorityCode: "the projection cannot be checked, and reading one datum's coordinates as another's is silent",
|
|
130
|
+
epsgMismatch:
|
|
131
|
+
"SSURGO publishes geographic WGS84, so a different code is a product change rather than a variation to absorb",
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
return { epsg: identity.epsg, featureCount: identity.featureCount, layer: identity.layer, bbox: identity.extent! }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The ingest's `SELECT`, with the FID range applied when one is asked for.
|
|
140
|
+
*/
|
|
141
|
+
function delineationSelectSQL(layer: string, options: SoilIngestOptions): string {
|
|
142
|
+
const select = `SELECT FID AS fid, MUKEY AS mukey, AREASYMBOL AS areasymbol FROM "${layer}"`
|
|
143
|
+
const bounds: string[] = []
|
|
144
|
+
|
|
145
|
+
if (options.fidFrom !== undefined) {
|
|
146
|
+
bounds.push(`FID >= ${options.fidFrom}`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (options.fidTo !== undefined) {
|
|
150
|
+
bounds.push(`FID <= ${options.fidTo}`)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return bounds.length ? `${select} WHERE ${bounds.join(" AND ")}` : select
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface RawFeature {
|
|
157
|
+
properties: { fid: number | string; mukey: number | string | null; areasymbol: string | null }
|
|
158
|
+
geometry: { type: string; coordinates: unknown } | null
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Stream the map-unit delineations as WGS84 features.
|
|
163
|
+
*
|
|
164
|
+
* Every feature is checked against the declared extent as it passes. A swapped coordinate order survives a projection
|
|
165
|
+
* check — both axes are still numbers in a plausible range — and shows up here immediately.
|
|
166
|
+
*
|
|
167
|
+
* @throws {Error} When ogr2ogr fails, when a feature carries no geometry or no `MUKEY`, or when a vertex falls outside
|
|
168
|
+
* the declared extent.
|
|
169
|
+
*/
|
|
170
|
+
export async function* readSoilDelineations(
|
|
171
|
+
options: SoilIngestOptions & { bbox: readonly [number, number, number, number] }
|
|
172
|
+
): AsyncGenerator<SoilDelineation> {
|
|
173
|
+
const layer = options.layer ?? basename(options.shapefilePath, ".shp")
|
|
174
|
+
const [minLon, minLat, maxLon, maxLat] = options.bbox
|
|
175
|
+
|
|
176
|
+
const args = [
|
|
177
|
+
"-f",
|
|
178
|
+
"GeoJSONSeq",
|
|
179
|
+
"/vsistdout/",
|
|
180
|
+
"-t_srs",
|
|
181
|
+
`EPSG:${options.expectEPSG ?? SSURGO_SOURCE_EPSG}`,
|
|
182
|
+
"-lco",
|
|
183
|
+
`COORDINATE_PRECISION=${COORDINATE_PRECISION}`,
|
|
184
|
+
...(options.limit === undefined ? [] : ["-limit", String(options.limit)]),
|
|
185
|
+
"-sql",
|
|
186
|
+
delineationSelectSQL(layer, options),
|
|
187
|
+
options.shapefilePath,
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
for await (const raw of ogr2ogrGeoJSONSeq<RawFeature>(args, "soil ingest")) {
|
|
191
|
+
yield toDelineation(raw, { minLon, minLat, maxLon, maxLat })
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Validate one raw GeoJSON feature and narrow it. Split out so the generator body stays a loop.
|
|
197
|
+
*/
|
|
198
|
+
function toDelineation(
|
|
199
|
+
raw: RawFeature,
|
|
200
|
+
extent: { minLon: number; minLat: number; maxLon: number; maxLat: number }
|
|
201
|
+
): SoilDelineation {
|
|
202
|
+
const { properties, geometry } = raw
|
|
203
|
+
|
|
204
|
+
if (!geometry) {
|
|
205
|
+
throw new Error(`soil ingest: delineation ${properties.fid} carries no geometry`)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (properties.mukey === null || properties.mukey === "") {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`soil ingest: delineation ${properties.fid} carries no MUKEY — a delineation with no map unit joins to nothing and would read downstream as unmapped ground`
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!properties.areasymbol) {
|
|
215
|
+
throw new Error(`soil ingest: delineation ${properties.fid} carries no AREASYMBOL`)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const polygons = requireArealPolygons(geometry, `delineation ${properties.fid}`, "soil ingest")
|
|
219
|
+
|
|
220
|
+
assertRingsInsideExtent(polygons, `delineation ${properties.fid}`, extent, BBOX_MARGIN_DEGREES, "soil ingest")
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
areaID: `${properties.areasymbol}:${properties.fid}`,
|
|
224
|
+
mukey: String(properties.mukey),
|
|
225
|
+
areasymbol: properties.areasymbol,
|
|
226
|
+
polygons,
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Where a build's delineations come from, and what the source declares about itself.
|
|
232
|
+
*
|
|
233
|
+
* The builder takes ONE of these rather than a path, which is what makes the fixture rung possible: hand-built geometry
|
|
234
|
+
* with no network and no GDAL still exercises the whole database half — the domain check, the cell classification, the
|
|
235
|
+
* reduction, the coverage rows, the manifest and the seal.
|
|
236
|
+
*/
|
|
237
|
+
export interface SoilFeatureSource {
|
|
238
|
+
areaSymbol: string
|
|
239
|
+
/**
|
|
240
|
+
* What the source says it holds. The build compares its own streamed total against this, so a short read throws
|
|
241
|
+
* instead of building a smaller county.
|
|
242
|
+
*/
|
|
243
|
+
declaredFeatureCount: number
|
|
244
|
+
layer: string
|
|
245
|
+
epsg: number
|
|
246
|
+
/**
|
|
247
|
+
* A description of where these delineations came from, for the receipt.
|
|
248
|
+
*/
|
|
249
|
+
origin: string
|
|
250
|
+
delineations: () => AsyncIterable<SoilDelineation>
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* One survey area's map-unit shapefile as a feature source — identity read up front, features streamed on demand.
|
|
255
|
+
*/
|
|
256
|
+
export async function createShapefileFeatureSource(
|
|
257
|
+
options: SoilIngestOptions & { areaSymbol: string; declaredFeatureCount?: number }
|
|
258
|
+
): Promise<SoilFeatureSource> {
|
|
259
|
+
const identity = await readSoilSourceIdentity(options)
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
areaSymbol: options.areaSymbol,
|
|
263
|
+
// A RANGE's own count is supplied by the caller, because `ogrinfo` reports the layer's total and nothing narrower.
|
|
264
|
+
// The whole-file total is still checked: the builder sums what its chunks streamed and compares that.
|
|
265
|
+
declaredFeatureCount: options.declaredFeatureCount ?? options.limit ?? identity.featureCount,
|
|
266
|
+
layer: identity.layer,
|
|
267
|
+
epsg: identity.epsg,
|
|
268
|
+
origin: options.shapefilePath,
|
|
269
|
+
delineations: () => readSoilDelineations({ ...options, bbox: identity.bbox }),
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* The index resolution is a MEASUREMENT this layer takes, not a number argued to.
|
|
7
|
+
*
|
|
8
|
+
* ONE STREAM, EVERY RESOLUTION. Re-reading a survey area's shapefile per candidate buys nothing — the
|
|
9
|
+
* classification is per delineation, so every candidate index folds the same delineation in turn. The cost
|
|
10
|
+
* is memory: each resolution holds its own cell sets, and the finest candidate dominates.
|
|
11
|
+
*
|
|
12
|
+
* THIS INSTRUMENT REPORTS THE FIRST OF THE TWO NUMBERS §4.7 NAMES — the `partial` cell share, plus the mean
|
|
13
|
+
* delineations per cell that drives it. The SECOND number, the share of cells whose top class holds less
|
|
14
|
+
* than half the cell, is not measurable here: it needs the attribute join and the area weighting, which
|
|
15
|
+
* are the build. So it comes off the SHIPPING ARTIFACT instead — {@linkcode buildSoilDatabase} counts it
|
|
16
|
+
* while it writes the rows, and the build receipt reports it. That is the flood layer's lesson applied:
|
|
17
|
+
* the number that describes the artifact is the one taken from the artifact.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { ResolutionMeasurementOptions } from "@mailwoman/core/layers"
|
|
21
|
+
|
|
22
|
+
import { classifyDelineationCells, SoilCellIndex, type SoilCellIndexMeasurement } from "#sdk/cells"
|
|
23
|
+
import { readSoilDelineations, readSoilSourceIdentity, type SoilIngestOptions } from "#sdk/ingest"
|
|
24
|
+
|
|
25
|
+
export interface MeasureSoilResolutionsOptions extends SoilIngestOptions, ResolutionMeasurementOptions {}
|
|
26
|
+
|
|
27
|
+
export interface SoilResolutionReport {
|
|
28
|
+
delineations: number
|
|
29
|
+
/**
|
|
30
|
+
* The count the shapefile declares for itself. A run whose streamed total differs read a truncated file.
|
|
31
|
+
*/
|
|
32
|
+
declaredFeatureCount: number
|
|
33
|
+
measurements: SoilCellIndexMeasurement[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const DEFAULT_PROGRESS_EVERY = 5000
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Measure every candidate resolution over one survey area's real delineations.
|
|
40
|
+
*
|
|
41
|
+
* @throws {Error} When the streamed count does not match the count the shapefile declares. A short read produces a
|
|
42
|
+
* well-formed table describing a smaller county, which is the partial result that must throw.
|
|
43
|
+
*/
|
|
44
|
+
export async function measureSoilCellResolutions(
|
|
45
|
+
options: MeasureSoilResolutionsOptions
|
|
46
|
+
): Promise<SoilResolutionReport> {
|
|
47
|
+
const identity = await readSoilSourceIdentity(options)
|
|
48
|
+
const indexes = options.resolutions.map((resolution) => new SoilCellIndex(resolution))
|
|
49
|
+
const progressEvery = options.progressEvery ?? DEFAULT_PROGRESS_EVERY
|
|
50
|
+
|
|
51
|
+
let delineations = 0
|
|
52
|
+
|
|
53
|
+
for await (const delineation of readSoilDelineations({ ...options, bbox: identity.bbox })) {
|
|
54
|
+
for (const index of indexes) {
|
|
55
|
+
index.add(
|
|
56
|
+
delineation.areaID,
|
|
57
|
+
classifyDelineationCells(delineation.polygons, index.resolution, delineation.areaID)
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
delineations++
|
|
62
|
+
|
|
63
|
+
if (delineations % progressEvery === 0) {
|
|
64
|
+
options.onProgress?.(`${delineations.toLocaleString()} delineations classified`)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const expected = options.limit ?? identity.featureCount
|
|
69
|
+
|
|
70
|
+
if (delineations !== expected) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`soil measure: streamed ${delineations} delineations, the shapefile declares ${expected} — a short read reports a smaller survey area rather than failing`
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
delineations,
|
|
78
|
+
declaredFeatureCount: identity.featureCount,
|
|
79
|
+
measurements: indexes.map((index) => index.finish()),
|
|
80
|
+
}
|
|
81
|
+
}
|