@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,778 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Build `soil.db` — the sealed polygon layer, from the survey areas NRCS publishes.
|
|
7
|
+
*
|
|
8
|
+
* THE ACCUMULATION IS IN SQL, NOT IN A MAP. Classification is per delineation and shared with the
|
|
9
|
+
* resolution measurement, but where the touches GO differs on purpose: the measuring instrument holds
|
|
10
|
+
* them in memory because it is comparing candidate resolutions in one pass, and the builder streams them
|
|
11
|
+
* into a temporary table because memory has to stay flat in row count. A polygon layer's touches
|
|
12
|
+
* outnumber its features, and Iowa is 99 survey areas.
|
|
13
|
+
*
|
|
14
|
+
* ABSENCE IS NO ROW, IN EVERY TABLE. Land outside a published survey area gets no `layer_coverage` row and
|
|
15
|
+
* no summary row — never a zero, never an empty histogram. Inside a published area the coverage row says
|
|
16
|
+
* `designated` at completeness 1.0, because NRCS declares its own mapping complete for those areas at its
|
|
17
|
+
* own scale; a coverage cell reached ONLY by `NOTCOM` and access-denied polygons gets no row either,
|
|
18
|
+
* because the polygon exists and the soil mapping behind it does not.
|
|
19
|
+
*
|
|
20
|
+
* A COVERAGE ROW LICENSES ONLY THAT THE AUTHORITY MAPPED HERE. The reading is the class distribution, and
|
|
21
|
+
* a cell that is 100% `unrated_share` is `designated`-complete and carries no capability reading
|
|
22
|
+
* whatsoever. That pairing is not a corner case: 17.1% of national components carry no capability rating,
|
|
23
|
+
* and for the irrigated rating the figure is 85.1%.
|
|
24
|
+
*
|
|
25
|
+
* THE AREA CROSS-CHECK USES THE AUTHORITY'S OWN PUBLISHED FIGURE. `legend.areaacres` is what NRCS states
|
|
26
|
+
* the survey area covers — 378,800 acres for `IA153`, which is 1,532.9 km² against the 1,532.5 km² an
|
|
27
|
+
* independent projected measurement of the same shapefile reports. Comparing the spherical ring sum
|
|
28
|
+
* against it catches a hole read as an exterior ring, whose absence is silent: such a polygon is
|
|
29
|
+
* well-formed and simply answers "inside" for ground the authority did not map.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { readFileSize } from "@mailwoman/core/fs/readers"
|
|
33
|
+
import {
|
|
34
|
+
areaAgreementFrom,
|
|
35
|
+
createLayerCoverageTable,
|
|
36
|
+
createLayerManifestTable,
|
|
37
|
+
designatedCoverageCells,
|
|
38
|
+
polygonLayerManifest,
|
|
39
|
+
writeLayerCoverage,
|
|
40
|
+
writeLayerManifest,
|
|
41
|
+
type AreaAgreementReading,
|
|
42
|
+
type CoverageCell,
|
|
43
|
+
} from "@mailwoman/core/layers"
|
|
44
|
+
import { resolveModulePath } from "@mailwoman/core/module/resolvers"
|
|
45
|
+
import { ingestChunkArguments, mergeCountsInto, runChunkProcess } from "@mailwoman/core/utils"
|
|
46
|
+
import {
|
|
47
|
+
arealPolygons,
|
|
48
|
+
geometryContains,
|
|
49
|
+
interiorCoverageCells,
|
|
50
|
+
shortCellToInt,
|
|
51
|
+
type MultiPolygonRings,
|
|
52
|
+
type ParsedGeometry,
|
|
53
|
+
} from "@mailwoman/spatial"
|
|
54
|
+
import type { DatabaseClient } from "@mailwoman/sqlite/client"
|
|
55
|
+
import { buildSealedArtifact } from "@mailwoman/sqlite/sealed-build"
|
|
56
|
+
import { cellToLatLng } from "h3-js"
|
|
57
|
+
|
|
58
|
+
import { createSoilTables, type SoilDatabase, type SoilSurveyAreaTable } from "#schema"
|
|
59
|
+
import { reduceCells, resolveCells } from "#sdk/cell-tiers"
|
|
60
|
+
import type { SoilFeatureSource } from "#sdk/ingest"
|
|
61
|
+
import type { SoilChunkResult } from "#sdk/ingest-chunk"
|
|
62
|
+
import { ingestSoilChunk } from "#sdk/ingest-chunk"
|
|
63
|
+
import { WEIGHT_LATTICE_DEPTH } from "#sdk/reduce"
|
|
64
|
+
import type { SurveyAreaAttributes } from "#sdk/survey-area"
|
|
65
|
+
import {
|
|
66
|
+
SOIL_SHARE_WEIGHTING,
|
|
67
|
+
SOIL_SHARE_WEIGHTING_DESCRIPTION,
|
|
68
|
+
soilLayerName,
|
|
69
|
+
SSURGO_ATTRIBUTION,
|
|
70
|
+
SSURGO_LICENSE,
|
|
71
|
+
SSURGO_SOURCE,
|
|
72
|
+
} from "#vocabulary"
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Schema version of the domain tables. Bumped when a column changes meaning, never for an added column a reader can
|
|
76
|
+
* ignore.
|
|
77
|
+
*/
|
|
78
|
+
export const SOIL_SCHEMA_VERSION = 1
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Delineation ids per chunk process.
|
|
82
|
+
*
|
|
83
|
+
* Sized against the ceiling the flood layer measured rather than guessed: single-process runs over that product died
|
|
84
|
+
* after roughly 510,000 and 798,000 features as h3's WASM heap fragmented. 100,000 leaves five times that margin, and
|
|
85
|
+
* the cost of a smaller number is one interpreter start per chunk. Iowa's largest survey area holds well under it, so
|
|
86
|
+
* on this build the bound costs one process per area — which is what makes an area's failure nameable.
|
|
87
|
+
*/
|
|
88
|
+
export const DEFAULT_CHUNK_SIZE = 100_000
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Square metres in an acre — the unit `legend.areaacres` publishes in.
|
|
92
|
+
*/
|
|
93
|
+
const M2_PER_ACRE = 4046.8564224
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The relative gap between the ring-area total and the authority's own published acreage that fails the build.
|
|
97
|
+
*
|
|
98
|
+
* Two percent. The comparison is a spherical ring area against a figure NRCS itself warns "may differ from that
|
|
99
|
+
* measured using GIS software due to different measuring techniques and rounding practices, or due to the fact that the
|
|
100
|
+
* value has been adjusted so that the sum total of all map units in the legend equals that listed for soil survey area"
|
|
101
|
+
* — so an exact test would be brittle. Two percent sits far above the 0.03% `IA153` measures and far below the error a
|
|
102
|
+
* hole-blind read produces, which the zoning survey measured at 4.1% over a whole national layer.
|
|
103
|
+
*/
|
|
104
|
+
const AREA_TOLERANCE = 0.02
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* One survey area, ready to build: where its geometry is, and what its tabular export said.
|
|
108
|
+
*/
|
|
109
|
+
export interface SurveyAreaInput {
|
|
110
|
+
attributes: SurveyAreaAttributes
|
|
111
|
+
/**
|
|
112
|
+
* The map-unit polygon shapefile.
|
|
113
|
+
*/
|
|
114
|
+
shapefilePath?: string
|
|
115
|
+
/**
|
|
116
|
+
* The survey area's own outline, already read.
|
|
117
|
+
*/
|
|
118
|
+
outline: ParsedGeometry
|
|
119
|
+
/**
|
|
120
|
+
* An in-process feature source. Correct for a fixture and for anything small; the batched path builds one of these
|
|
121
|
+
* per chunk, so the two share one implementation.
|
|
122
|
+
*/
|
|
123
|
+
source?: SoilFeatureSource
|
|
124
|
+
/**
|
|
125
|
+
* The delineation count the shapefile declares, when the caller has already read it.
|
|
126
|
+
*/
|
|
127
|
+
declaredFeatureCount?: number
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface BuildSoilOptions {
|
|
131
|
+
/**
|
|
132
|
+
* The survey areas to build, in the order they should be ingested.
|
|
133
|
+
*/
|
|
134
|
+
areas: ReadonlyArray<SurveyAreaInput>
|
|
135
|
+
/**
|
|
136
|
+
* The region the layer name carries — the pilot's is `ia`.
|
|
137
|
+
*/
|
|
138
|
+
region: string
|
|
139
|
+
/**
|
|
140
|
+
* Where the sealed artifact lands. The build writes beside it and swaps.
|
|
141
|
+
*/
|
|
142
|
+
out: string
|
|
143
|
+
/**
|
|
144
|
+
* The refresh the build ingested — `layer_manifest.version` and `source_vintage`.
|
|
145
|
+
*/
|
|
146
|
+
sourceVintage: string
|
|
147
|
+
buildCmd: string
|
|
148
|
+
buildSHA: string
|
|
149
|
+
/**
|
|
150
|
+
* ISO-8601, supplied by the caller. Never generated here: the contract says so, and a library-generated timestamp
|
|
151
|
+
* makes two builds of the same inputs differ.
|
|
152
|
+
*/
|
|
153
|
+
createdAt: string
|
|
154
|
+
/**
|
|
155
|
+
* The resolution the cell index and the reduction are built at — chosen from the measurement.
|
|
156
|
+
*/
|
|
157
|
+
indexResolution: number
|
|
158
|
+
/**
|
|
159
|
+
* The resolution `layer_coverage` rows are keyed at. Must be coarser than the index resolution.
|
|
160
|
+
*/
|
|
161
|
+
coverageResolution: number
|
|
162
|
+
/**
|
|
163
|
+
* Delineation ids per chunk process. See {@link DEFAULT_CHUNK_SIZE}.
|
|
164
|
+
*/
|
|
165
|
+
chunkSize?: number
|
|
166
|
+
/**
|
|
167
|
+
* Run the ingest IN THIS PROCESS rather than spawning chunk children. Only for fixtures, which carry no shapefile for
|
|
168
|
+
* a child to open.
|
|
169
|
+
*/
|
|
170
|
+
inProcess?: boolean
|
|
171
|
+
onProgress?: (message: string) => void
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface BuildSoilResult {
|
|
175
|
+
out: string
|
|
176
|
+
region: string
|
|
177
|
+
surveyAreas: number
|
|
178
|
+
delineations: number
|
|
179
|
+
mapUnits: number
|
|
180
|
+
components: number
|
|
181
|
+
indexResolution: number
|
|
182
|
+
coverageResolution: number
|
|
183
|
+
wholeCellRows: number
|
|
184
|
+
partialCellRows: number
|
|
185
|
+
/**
|
|
186
|
+
* `partialCellRows / (wholeCellRows + partialCellRows)` over the STORED index rows. The whole side is compacted, so
|
|
187
|
+
* this is not the same number the resolution was chosen on and is reported separately.
|
|
188
|
+
*/
|
|
189
|
+
storedPartialShare: number
|
|
190
|
+
capabilityCells: number
|
|
191
|
+
/**
|
|
192
|
+
* Cells the lattice was used for, rather than the whole-cell fast path.
|
|
193
|
+
*/
|
|
194
|
+
sampledCells: number
|
|
195
|
+
/**
|
|
196
|
+
* Cells whose top class covers less than half the cell — the §4.7 number, taken off the shipping artifact rather than
|
|
197
|
+
* out of a separate harness.
|
|
198
|
+
*/
|
|
199
|
+
topClassUnderHalfCells: number
|
|
200
|
+
topClassUnderHalfShare: number
|
|
201
|
+
/**
|
|
202
|
+
* Cells with NO class at all: the survey mapped them and rated nothing there.
|
|
203
|
+
*/
|
|
204
|
+
classlessCells: number
|
|
205
|
+
/**
|
|
206
|
+
* Cells the index touched that no lattice point landed inside. Dropped rather than stored as an all-zero
|
|
207
|
+
* distribution, and counted because a large number would mean the lattice is too coarse for this geometry.
|
|
208
|
+
*/
|
|
209
|
+
unsampledCells: number
|
|
210
|
+
meanDelineationsPerCell: number
|
|
211
|
+
coarsenedFeatures: number
|
|
212
|
+
storedResolutions: number[]
|
|
213
|
+
coverageCells: number
|
|
214
|
+
/**
|
|
215
|
+
* Coverage cells inside a survey-area outline that no delineation with soil mapping reached. Reported rather than
|
|
216
|
+
* smoothed over: SSURGO is wall-to-wall inside a published area, so a large number means the outline and the
|
|
217
|
+
* delineations disagree.
|
|
218
|
+
*/
|
|
219
|
+
coverageCellsWithoutMapping: number
|
|
220
|
+
/**
|
|
221
|
+
* The area readings in square kilometres against the authority's own published figure, with the witness stated. The
|
|
222
|
+
* `known` count of areas that publish an acreage is what a receipt reads the absence against.
|
|
223
|
+
*/
|
|
224
|
+
area: AreaAgreementReading
|
|
225
|
+
sizeBytes: number
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Build the layer.
|
|
230
|
+
*
|
|
231
|
+
* @throws {Error} On a delineation the classifier refuses, a streamed count that disagrees with the shapefile's own
|
|
232
|
+
* declaration, an area total that disagrees with the authority's published acreage, or a set of outlines that yields
|
|
233
|
+
* no interior coverage cell at all.
|
|
234
|
+
*/
|
|
235
|
+
export async function buildSoilDatabase(options: BuildSoilOptions): Promise<BuildSoilResult> {
|
|
236
|
+
if (options.coverageResolution >= options.indexResolution) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`soil build: the coverage resolution (${options.coverageResolution}) must be coarser than the index resolution (${options.indexResolution}) — a row's coverage cell is the PARENT of its index cell`
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!options.areas.length) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
"soil build: no survey areas were supplied — an empty build writes an artifact that answers unknown everywhere while reporting success"
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const built = await buildSealedArtifact<SoilDatabase, StreamResult, Omit<BuildSoilResult, "sizeBytes">>({
|
|
249
|
+
out: options.out,
|
|
250
|
+
createTables: async (kdb) => {
|
|
251
|
+
await createSoilTables(kdb)
|
|
252
|
+
await createLayerManifestTable(kdb)
|
|
253
|
+
await createLayerCoverageTable(kdb)
|
|
254
|
+
|
|
255
|
+
// The touch table exists only for this build and is dropped before the artifact is sealed. No primary key while
|
|
256
|
+
// loading: the resolution queries below read it through indexes created once the load is done, and a clustered
|
|
257
|
+
// key would sort every insert against an ingest order nothing controls.
|
|
258
|
+
kdb.exec(
|
|
259
|
+
"CREATE TABLE build_cell_touch (h3_cell INTEGER NOT NULL, resolution INTEGER NOT NULL, area_id TEXT NOT NULL, is_full INTEGER NOT NULL)"
|
|
260
|
+
)
|
|
261
|
+
},
|
|
262
|
+
ingest: async (kdb) => {
|
|
263
|
+
// Attributes FIRST because the ingest needs one thing out of them — which map units have no soil mapping behind
|
|
264
|
+
// them — and because a delineation whose map unit is missing must fail while the artifact is still empty rather
|
|
265
|
+
// than after millions of geometry rows are written.
|
|
266
|
+
writeAttributes(kdb, options.areas)
|
|
267
|
+
|
|
268
|
+
if (!options.inProcess) return undefined
|
|
269
|
+
|
|
270
|
+
return aggregateChunks(await ingestInProcess(kdb, options))
|
|
271
|
+
},
|
|
272
|
+
...(options.inProcess
|
|
273
|
+
? {}
|
|
274
|
+
: { batched: async (tmpPath: string) => aggregateChunks(await runBatchedIngest(tmpPath, options)) }),
|
|
275
|
+
finish: async (kdb, ingested) => {
|
|
276
|
+
assertDelineationCounts(options.areas, ingested)
|
|
277
|
+
const area = assertAreaAgreement(options.areas, ingested)
|
|
278
|
+
|
|
279
|
+
options.onProgress?.(`${ingested.delineations.toLocaleString()} delineations written · resolving cells`)
|
|
280
|
+
|
|
281
|
+
const cells = resolveCells(kdb, options.indexResolution)
|
|
282
|
+
|
|
283
|
+
options.onProgress?.(
|
|
284
|
+
`${cells.wholeRows.toLocaleString()} whole (compacted) · ${cells.partialRows.toLocaleString()} partial · reducing`
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
const reduced = reduceCells(kdb, options.indexResolution, options.onProgress)
|
|
288
|
+
|
|
289
|
+
const coverage = buildCoverageCells(options, ingested)
|
|
290
|
+
|
|
291
|
+
await writeLayerCoverage(kdb, coverage.cells)
|
|
292
|
+
|
|
293
|
+
writeSurveyAreaRows(kdb, options, coverage.cellsByArea)
|
|
294
|
+
writeVocabularyRows(kdb, options.areas)
|
|
295
|
+
|
|
296
|
+
// THE SPINE KEY NAMES THE TABLE A CONSUMER JOINS ON, table-qualified, per the layer contract. For this layer
|
|
297
|
+
// that is the REDUCTION rather than the containment index: `soil_capability_cell` holds one row per cell at one
|
|
298
|
+
// resolution, which `soil_map_unit_cell` does not — it is keyed `(cell, delineation)` and is mixed-resolution by
|
|
299
|
+
// construction, so it is a tier the reader walks rather than a key a consumer joins.
|
|
300
|
+
await writeLayerManifest(
|
|
301
|
+
kdb,
|
|
302
|
+
polygonLayerManifest(options, {
|
|
303
|
+
name: soilLayerName(options.region),
|
|
304
|
+
schemaVersion: SOIL_SCHEMA_VERSION,
|
|
305
|
+
license: SSURGO_LICENSE,
|
|
306
|
+
attribution: SSURGO_ATTRIBUTION,
|
|
307
|
+
source: SSURGO_SOURCE,
|
|
308
|
+
cellColumn: "soil_capability_cell.h3_cell",
|
|
309
|
+
})
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
kdb.exec("DROP TABLE build_cell_touch")
|
|
313
|
+
|
|
314
|
+
const totalCellRows = cells.wholeRows + cells.partialRows
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
out: options.out,
|
|
318
|
+
region: options.region,
|
|
319
|
+
surveyAreas: options.areas.length,
|
|
320
|
+
delineations: ingested.delineations,
|
|
321
|
+
mapUnits: options.areas.reduce((sum, input) => sum + input.attributes.mapUnits.length, 0),
|
|
322
|
+
components: options.areas.reduce((sum, input) => sum + input.attributes.components.length, 0),
|
|
323
|
+
indexResolution: options.indexResolution,
|
|
324
|
+
coverageResolution: options.coverageResolution,
|
|
325
|
+
wholeCellRows: cells.wholeRows,
|
|
326
|
+
partialCellRows: cells.partialRows,
|
|
327
|
+
storedPartialShare: totalCellRows ? cells.partialRows / totalCellRows : 0,
|
|
328
|
+
capabilityCells: reduced.cells,
|
|
329
|
+
sampledCells: reduced.sampled,
|
|
330
|
+
topClassUnderHalfCells: reduced.topClassUnderHalf,
|
|
331
|
+
topClassUnderHalfShare: reduced.cells ? reduced.topClassUnderHalf / reduced.cells : 0,
|
|
332
|
+
classlessCells: reduced.classless,
|
|
333
|
+
unsampledCells: reduced.unsampled,
|
|
334
|
+
meanDelineationsPerCell: reduced.cells ? reduced.candidatePairs / reduced.cells : 0,
|
|
335
|
+
coarsenedFeatures: ingested.coarsened,
|
|
336
|
+
storedResolutions: cells.resolutions,
|
|
337
|
+
coverageCells: coverage.cells.length,
|
|
338
|
+
coverageCellsWithoutMapping: coverage.withoutMapping,
|
|
339
|
+
area,
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
return { ...built, sizeBytes: await readFileSize(options.out) }
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* What the whole ingest produced, however many processes it took.
|
|
349
|
+
*/
|
|
350
|
+
interface StreamResult {
|
|
351
|
+
delineations: number
|
|
352
|
+
coarsened: number
|
|
353
|
+
byArea: Map<string, number>
|
|
354
|
+
observedByCoverageCell: Map<number, number>
|
|
355
|
+
mappedByCoverageCell: Map<number, number>
|
|
356
|
+
nestedM2: number
|
|
357
|
+
allExteriorM2: number
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Add up what the chunks reported.
|
|
362
|
+
*
|
|
363
|
+
* Exported for its own test: the coverage-cell arithmetic is the one part of the batched path a fixture build cannot
|
|
364
|
+
* reach, and getting it wrong produces a well-formed artifact that under-reports how many delineations a cell holds.
|
|
365
|
+
*/
|
|
366
|
+
export function aggregateChunks(chunks: ReadonlyArray<SoilChunkResult>): StreamResult {
|
|
367
|
+
const byArea = new Map<string, number>()
|
|
368
|
+
const observedByCoverageCell = new Map<number, number>()
|
|
369
|
+
const mappedByCoverageCell = new Map<number, number>()
|
|
370
|
+
|
|
371
|
+
let delineations = 0
|
|
372
|
+
let coarsened = 0
|
|
373
|
+
let nestedM2 = 0
|
|
374
|
+
let allExteriorM2 = 0
|
|
375
|
+
|
|
376
|
+
for (const chunk of chunks) {
|
|
377
|
+
delineations += chunk.delineations
|
|
378
|
+
coarsened += chunk.coarsened
|
|
379
|
+
nestedM2 += chunk.area.nestedM2
|
|
380
|
+
allExteriorM2 += chunk.area.allExteriorM2
|
|
381
|
+
|
|
382
|
+
byArea.set(chunk.areaSymbol, (byArea.get(chunk.areaSymbol) ?? 0) + chunk.delineations)
|
|
383
|
+
|
|
384
|
+
// A coverage cell straddles chunk boundaries — a range of feature ids is not a region, and a coverage cell can
|
|
385
|
+
// straddle two survey areas — so the counts ADD rather than replace.
|
|
386
|
+
mergeCountsInto(observedByCoverageCell, chunk.observedByCoverageCell)
|
|
387
|
+
mergeCountsInto(mappedByCoverageCell, chunk.mappedByCoverageCell)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return { delineations, coarsened, byArea, observedByCoverageCell, mappedByCoverageCell, nestedM2, allExteriorM2 }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Refuse a build that streamed fewer delineations than a survey area declares.
|
|
395
|
+
*
|
|
396
|
+
* A short read builds a smaller county and reports success, which is the partial result that must throw.
|
|
397
|
+
*/
|
|
398
|
+
function assertDelineationCounts(areas: ReadonlyArray<SurveyAreaInput>, streamed: StreamResult): void {
|
|
399
|
+
for (const area of areas) {
|
|
400
|
+
const declared = area.declaredFeatureCount ?? area.source?.declaredFeatureCount
|
|
401
|
+
|
|
402
|
+
if (declared === undefined) continue
|
|
403
|
+
|
|
404
|
+
const actual = streamed.byArea.get(area.attributes.areasymbol) ?? 0
|
|
405
|
+
|
|
406
|
+
if (actual !== declared) {
|
|
407
|
+
throw new Error(
|
|
408
|
+
`soil build: ${area.attributes.areasymbol} streamed ${actual} delineations, its shapefile declares ${declared} — a short read builds a smaller survey area and reports success`
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Refuse an artifact whose rings do not add up to the acreage the authority publishes.
|
|
416
|
+
*
|
|
417
|
+
* The message carries the hole-blind total beside the nested one, because the gap between them is the diagnosis: a hole
|
|
418
|
+
* read as an exterior ring answers "inside" for every point in it. A build over survey areas that publish no acreage
|
|
419
|
+
* has no witness — the reading's own type says so, and the `known` count is what a receipt names.
|
|
420
|
+
*/
|
|
421
|
+
function assertAreaAgreement(areas: ReadonlyArray<SurveyAreaInput>, streamed: StreamResult): AreaAgreementReading {
|
|
422
|
+
let publishedAcres = 0
|
|
423
|
+
let known = 0
|
|
424
|
+
|
|
425
|
+
for (const input of areas) {
|
|
426
|
+
if (input.attributes.areaAcres !== null) {
|
|
427
|
+
publishedAcres += input.attributes.areaAcres
|
|
428
|
+
|
|
429
|
+
known++
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const area = areaAgreementFrom(
|
|
434
|
+
{ nestedM2: streamed.nestedM2, allExteriorM2: streamed.allExteriorM2 },
|
|
435
|
+
known ? publishedAcres * M2_PER_ACRE : undefined
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
if (area.witness === "source" && area.relativeGap > AREA_TOLERANCE) {
|
|
439
|
+
throw new Error(
|
|
440
|
+
`soil build: the encoded rings total ${area.nestedKM2.toFixed(1)} km² against the ${area.sourceKM2.toFixed(1)} km² the authority publishes for these ${known} survey areas ` +
|
|
441
|
+
`(${(area.relativeGap * 100).toFixed(2)}% apart, tolerance ${(AREA_TOLERANCE * 100).toFixed(0)}%). Read without their holes the same rings total ` +
|
|
442
|
+
`${area.allExteriorKM2.toFixed(1)} km², so compare the two: a hole-blind read answers "inside" for every point in a hole`
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return area
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Write every survey area's map units, components and vocabulary before any geometry is streamed.
|
|
451
|
+
*/
|
|
452
|
+
function writeAttributes(database: DatabaseClient<SoilDatabase>, areas: ReadonlyArray<SurveyAreaInput>): void {
|
|
453
|
+
const insertMapUnit = database.prepare(
|
|
454
|
+
"INSERT OR REPLACE INTO soil_map_unit (mukey, areasymbol, musym, muname, mukind, mustatus, farmlndcl, farmland_scope, niccdcd, niccdcdpct, no_mapping) " +
|
|
455
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
const insertComponent = database.prepare(
|
|
459
|
+
"INSERT OR REPLACE INTO soil_component (cokey, mukey, comppct_r, compname, compkind, nirrcapcl, nirrcapscl, irrcapcl, irrcapscl, nccpi_v3) " +
|
|
460
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
database.exec("BEGIN")
|
|
464
|
+
|
|
465
|
+
for (const area of areas) {
|
|
466
|
+
for (const mapUnit of area.attributes.mapUnits) {
|
|
467
|
+
insertMapUnit.run(
|
|
468
|
+
mapUnit.mukey,
|
|
469
|
+
mapUnit.areasymbol,
|
|
470
|
+
mapUnit.musym,
|
|
471
|
+
mapUnit.muname,
|
|
472
|
+
mapUnit.mukind,
|
|
473
|
+
mapUnit.mustatus,
|
|
474
|
+
mapUnit.farmlndcl,
|
|
475
|
+
mapUnit.farmland_scope,
|
|
476
|
+
mapUnit.niccdcd,
|
|
477
|
+
mapUnit.niccdcdpct,
|
|
478
|
+
mapUnit.no_mapping
|
|
479
|
+
)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
for (const component of area.attributes.components) {
|
|
483
|
+
insertComponent.run(
|
|
484
|
+
component.cokey,
|
|
485
|
+
component.mukey,
|
|
486
|
+
component.comppct_r,
|
|
487
|
+
component.compname,
|
|
488
|
+
component.compkind,
|
|
489
|
+
component.nirrcapcl,
|
|
490
|
+
component.nirrcapscl,
|
|
491
|
+
component.irrcapcl,
|
|
492
|
+
component.irrcapscl,
|
|
493
|
+
component.nccpi_v3
|
|
494
|
+
)
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
database.exec("COMMIT")
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* The map units with no soil mapping behind them, from what was just written.
|
|
503
|
+
*/
|
|
504
|
+
function noMappingMukeys(areas: ReadonlyArray<SurveyAreaInput>): Set<string> {
|
|
505
|
+
const mukeys = new Set<string>()
|
|
506
|
+
|
|
507
|
+
for (const area of areas) {
|
|
508
|
+
for (const mapUnit of area.attributes.mapUnits) {
|
|
509
|
+
if (mapUnit.no_mapping) {
|
|
510
|
+
mukeys.add(mapUnit.mukey)
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return mukeys
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* The in-process ingest — one chunk per survey area, all in this interpreter. Fixtures only.
|
|
520
|
+
*/
|
|
521
|
+
async function ingestInProcess(
|
|
522
|
+
database: DatabaseClient<SoilDatabase>,
|
|
523
|
+
options: BuildSoilOptions
|
|
524
|
+
): Promise<SoilChunkResult[]> {
|
|
525
|
+
const noMapping = noMappingMukeys(options.areas)
|
|
526
|
+
const chunks: SoilChunkResult[] = []
|
|
527
|
+
|
|
528
|
+
for (const area of options.areas) {
|
|
529
|
+
if (!area.source) {
|
|
530
|
+
throw new Error(
|
|
531
|
+
`soil build: ${area.attributes.areasymbol} was built in-process without a feature source — the in-process path is for fixtures, which supply one`
|
|
532
|
+
)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
chunks.push(
|
|
536
|
+
await ingestSoilChunk(database, {
|
|
537
|
+
source: area.source,
|
|
538
|
+
indexResolution: options.indexResolution,
|
|
539
|
+
coverageResolution: options.coverageResolution,
|
|
540
|
+
noMappingMukeys: noMapping,
|
|
541
|
+
...(options.onProgress ? { onProgress: options.onProgress } : {}),
|
|
542
|
+
})
|
|
543
|
+
)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return chunks
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Run the ingest as a sequence of bounded child processes, one per range of one survey area's FIDs. The shared chunk
|
|
551
|
+
* contract — the parent's no-handle rule, and the fail-loud handling of a chunk that dies or prints nothing — lives
|
|
552
|
+
* with `ingestChunkArguments` and `runChunkProcess`.
|
|
553
|
+
*/
|
|
554
|
+
async function runBatchedIngest(tmpPath: string, options: BuildSoilOptions): Promise<SoilChunkResult[]> {
|
|
555
|
+
const chunkSize = options.chunkSize ?? DEFAULT_CHUNK_SIZE
|
|
556
|
+
const script = resolveModulePath("@mailwoman/soil/scripts/ingest-chunk")
|
|
557
|
+
const noMapping = [...noMappingMukeys(options.areas)]
|
|
558
|
+
const chunks: SoilChunkResult[] = []
|
|
559
|
+
|
|
560
|
+
for (const area of options.areas) {
|
|
561
|
+
if (!area.shapefilePath) {
|
|
562
|
+
throw new Error(
|
|
563
|
+
`soil build: ${area.attributes.areasymbol} has no shapefile path, so no chunk process can open it`
|
|
564
|
+
)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const declared = area.declaredFeatureCount ?? 0
|
|
568
|
+
|
|
569
|
+
for (let from = 0; from < Math.max(declared, 1); from += chunkSize) {
|
|
570
|
+
const to = Math.min(from + chunkSize - 1, declared - 1)
|
|
571
|
+
|
|
572
|
+
options.onProgress?.(`${area.attributes.areasymbol}: FID ${from}–${to}`)
|
|
573
|
+
|
|
574
|
+
chunks.push(
|
|
575
|
+
await runChunkProcess<SoilChunkResult>({
|
|
576
|
+
script,
|
|
577
|
+
context: "soil build",
|
|
578
|
+
subject: `chunk ${area.attributes.areasymbol} FID ${from}–${to}`,
|
|
579
|
+
args: ingestChunkArguments({
|
|
580
|
+
database: tmpPath,
|
|
581
|
+
args: [
|
|
582
|
+
"--shapefile",
|
|
583
|
+
area.shapefilePath,
|
|
584
|
+
"--area-symbol",
|
|
585
|
+
area.attributes.areasymbol,
|
|
586
|
+
"--fid-from",
|
|
587
|
+
String(from),
|
|
588
|
+
"--fid-to",
|
|
589
|
+
String(to),
|
|
590
|
+
"--no-mapping-mukeys",
|
|
591
|
+
noMapping.join(","),
|
|
592
|
+
],
|
|
593
|
+
indexResolution: options.indexResolution,
|
|
594
|
+
coverageResolution: options.coverageResolution,
|
|
595
|
+
}),
|
|
596
|
+
})
|
|
597
|
+
)
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return chunks
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* The coverage rows: one per interior cell of the built footprint that soil mapping actually reaches, and none outside.
|
|
606
|
+
*
|
|
607
|
+
* THE INTERIOR TEST RUNS ONCE OVER THE UNION OF EVERY OUTLINE BUILT, NOT PER SURVEY AREA, and the difference is most of
|
|
608
|
+
* a state. The test is conservative — it keeps only cells lying WHOLLY inside — so applied per area it drops every cell
|
|
609
|
+
* a county border crosses. Measured on Polk County alone at resolution 6: 20 interior cells against the roughly 42 the
|
|
610
|
+
* county spans by area, so more than half of it would read `unknown` while sitting inside a survey the build had
|
|
611
|
+
* ingested. Run over the union, only the OUTER border of the built set is dropped, which is the honest edge: beyond it
|
|
612
|
+
* lies ground this artifact does not hold.
|
|
613
|
+
*
|
|
614
|
+
* The conservatism itself stays. A cell wrongly called interior would state that an authority determined a location it
|
|
615
|
+
* never looked at, and a point in the dropped strip reading `unknown` is the truthful answer for ground the built set
|
|
616
|
+
* may or may not reach.
|
|
617
|
+
*
|
|
618
|
+
* `observed_rows` counts the delineations reaching the cell, which is what the contract's column means. A cell reached
|
|
619
|
+
* only by `NOTCOM` and access-denied polygons gets NO ROW — the polygon exists, the soil mapping behind it does not,
|
|
620
|
+
* and the survey's §3.2 puts that case with the absences rather than with the coverage.
|
|
621
|
+
*/
|
|
622
|
+
function buildCoverageCells(
|
|
623
|
+
options: BuildSoilOptions,
|
|
624
|
+
streamed: StreamResult
|
|
625
|
+
): { cells: CoverageCell[]; cellsByArea: Map<string, number>; withoutMapping: number } {
|
|
626
|
+
const footprint: ParsedGeometry = {
|
|
627
|
+
type: "MultiPolygon",
|
|
628
|
+
coordinates: options.areas.flatMap((input) => outlinePolygons(input.outline)),
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const interior = interiorCoverageCells(footprint, options.coverageResolution)
|
|
632
|
+
|
|
633
|
+
if (!interior.length) {
|
|
634
|
+
throw new Error(
|
|
635
|
+
`soil build: the ${options.areas.length} built outline(s) yield no interior cell at resolution ${options.coverageResolution} — the artifact would carry no coverage rows and answer "unknown" everywhere while reporting success`
|
|
636
|
+
)
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const mapped = (h3Cell: number): boolean => (streamed.mappedByCoverageCell.get(h3Cell) ?? 0) > 0
|
|
640
|
+
|
|
641
|
+
const cells = designatedCoverageCells(
|
|
642
|
+
interior.map((cell) => shortCellToInt(cell)),
|
|
643
|
+
streamed.observedByCoverageCell,
|
|
644
|
+
{ include: mapped }
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
const cellsByArea = new Map<string, number>()
|
|
648
|
+
|
|
649
|
+
let withoutMapping = 0
|
|
650
|
+
|
|
651
|
+
for (const cell of interior) {
|
|
652
|
+
if (!mapped(shortCellToInt(cell))) {
|
|
653
|
+
withoutMapping++
|
|
654
|
+
|
|
655
|
+
continue
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Attributed by the cell's CENTRE, so each row is counted for exactly one survey area even where the cell straddles
|
|
659
|
+
// two. The count is a per-area receipt, not part of the coverage claim — the claim is the row set itself.
|
|
660
|
+
const [latitude, longitude] = cellToLatLng(cell)
|
|
661
|
+
const owner = options.areas.find((input) => geometryContains(input.outline, longitude, latitude))
|
|
662
|
+
|
|
663
|
+
if (owner) {
|
|
664
|
+
const symbol = owner.attributes.areasymbol
|
|
665
|
+
|
|
666
|
+
cellsByArea.set(symbol, (cellsByArea.get(symbol) ?? 0) + 1)
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return { cells, cellsByArea, withoutMapping }
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* One outline's polygons, in the `MultiPolygon` coordinate shape, whichever areal type it arrived as.
|
|
675
|
+
*
|
|
676
|
+
* @throws {TypeError} When the outline is not areal. A survey area whose footprint cannot be read would silently
|
|
677
|
+
* contribute nothing to the union, and the coverage over it would simply be absent.
|
|
678
|
+
*/
|
|
679
|
+
function outlinePolygons(outline: ParsedGeometry): MultiPolygonRings {
|
|
680
|
+
const polygons = arealPolygons(outline)
|
|
681
|
+
|
|
682
|
+
if (polygons) return polygons
|
|
683
|
+
|
|
684
|
+
throw new TypeError(
|
|
685
|
+
`soil build: a survey-area outline is a ${outline.type}, which bounds no area — its coverage would be silently absent rather than refused`
|
|
686
|
+
)
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Insert one row per survey area.
|
|
691
|
+
*/
|
|
692
|
+
function writeSurveyAreaRows(
|
|
693
|
+
database: DatabaseClient<SoilDatabase>,
|
|
694
|
+
options: BuildSoilOptions,
|
|
695
|
+
cellsByArea: ReadonlyMap<string, number>
|
|
696
|
+
): void {
|
|
697
|
+
const insert = database.prepare(
|
|
698
|
+
"INSERT OR REPLACE INTO soil_survey_area (areasymbol, areaname, saverest, saversion, survey_source_date, survey_source_title, source_scale, mapping_scale, area_acres, min_lat, min_lon, max_lat, max_lon, coverage_cells, coverage_resolution) " +
|
|
699
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
database.exec("BEGIN")
|
|
703
|
+
|
|
704
|
+
for (const area of options.areas) {
|
|
705
|
+
const bounds = database
|
|
706
|
+
.prepare(
|
|
707
|
+
"SELECT min(min_lat) AS min_lat, min(min_lon) AS min_lon, max(max_lat) AS max_lat, max(max_lon) AS max_lon FROM soil_map_unit_area WHERE areasymbol = ?"
|
|
708
|
+
)
|
|
709
|
+
.get(area.attributes.areasymbol) as {
|
|
710
|
+
min_lat: number | null
|
|
711
|
+
min_lon: number | null
|
|
712
|
+
max_lat: number | null
|
|
713
|
+
max_lon: number | null
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const row: SoilSurveyAreaTable = {
|
|
717
|
+
areasymbol: area.attributes.areasymbol,
|
|
718
|
+
areaname: area.attributes.areaname,
|
|
719
|
+
saverest: area.attributes.saverest,
|
|
720
|
+
saversion: area.attributes.saversion,
|
|
721
|
+
survey_source_date: area.attributes.surveySourceDate,
|
|
722
|
+
survey_source_title: area.attributes.surveySourceTitle,
|
|
723
|
+
source_scale: area.attributes.sourceScale,
|
|
724
|
+
mapping_scale: area.attributes.mappingScale,
|
|
725
|
+
area_acres: area.attributes.areaAcres,
|
|
726
|
+
min_lat: bounds.min_lat ?? 0,
|
|
727
|
+
min_lon: bounds.min_lon ?? 0,
|
|
728
|
+
max_lat: bounds.max_lat ?? 0,
|
|
729
|
+
max_lon: bounds.max_lon ?? 0,
|
|
730
|
+
coverage_cells: cellsByArea.get(area.attributes.areasymbol) ?? 0,
|
|
731
|
+
coverage_resolution: options.coverageResolution,
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
insert.run(
|
|
735
|
+
row.areasymbol,
|
|
736
|
+
row.areaname,
|
|
737
|
+
row.saverest,
|
|
738
|
+
row.saversion,
|
|
739
|
+
row.survey_source_date,
|
|
740
|
+
row.survey_source_title,
|
|
741
|
+
row.source_scale,
|
|
742
|
+
row.mapping_scale,
|
|
743
|
+
row.area_acres,
|
|
744
|
+
row.min_lat,
|
|
745
|
+
row.min_lon,
|
|
746
|
+
row.max_lat,
|
|
747
|
+
row.max_lon,
|
|
748
|
+
row.coverage_cells,
|
|
749
|
+
row.coverage_resolution
|
|
750
|
+
)
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
database.exec("COMMIT")
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Insert the authority's declared domains, plus the weighting the shares were produced under.
|
|
758
|
+
*
|
|
759
|
+
* The weighting rides in the vocabulary table as well as on every row: the row-level copy is what a consumer reads, and
|
|
760
|
+
* this one carries the sentence that says what it MEANS, which no column can.
|
|
761
|
+
*/
|
|
762
|
+
function writeVocabularyRows(database: DatabaseClient<SoilDatabase>, areas: ReadonlyArray<SurveyAreaInput>): void {
|
|
763
|
+
const insert = database.prepare(
|
|
764
|
+
"INSERT OR REPLACE INTO soil_vocabulary (domain, code, definition, sequence) VALUES (?, ?, ?, ?)"
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
database.exec("BEGIN")
|
|
768
|
+
|
|
769
|
+
for (const area of areas) {
|
|
770
|
+
for (const member of area.attributes.domains) {
|
|
771
|
+
insert.run(member.domain, member.code, member.definition, member.sequence)
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
insert.run("share_weighting", SOIL_SHARE_WEIGHTING, SOIL_SHARE_WEIGHTING_DESCRIPTION, WEIGHT_LATTICE_DEPTH)
|
|
776
|
+
|
|
777
|
+
database.exec("COMMIT")
|
|
778
|
+
}
|