@larsgw/formica 0.9.2 → 0.10.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/lib/bin/process-resources-index.js +0 -0
- package/lib/bin/process-resources.d.ts +6 -1
- package/lib/bin/process-resources.js +116 -25
- package/lib/bin/validate-catalog.js +0 -0
- package/lib/bin/validate-resources-text.js +3 -1
- package/lib/resources/parse-text.js +6 -3
- package/lib/taxon-names/index.js +1 -1
- package/package.json +15 -13
- package/.gitattributes +0 -1
- package/.github/workflows/ci.yml +0 -27
- package/CHANGELOG.md +0 -390
- package/eslint.config.js +0 -34
- package/lib/bin/SHEETS.js +0 -0
- package/lib/bin/clean-links.d.ts +0 -2
- package/lib/bin/clean-links.js +0 -170
- package/lib/bin/download-place-shapes.js +0 -188
- package/lib/bin/index-place-shapes.js +0 -115
- package/lib/bin/process-resources-problems.js +0 -177
- package/lib/bin/validate-linked-data.js +0 -0
- package/lib/resources/content/clavis.js +0 -10
- package/lib/resources/content/index.js +0 -0
- package/lib/resources/content/sdd.js +0 -151
- package/lib/resources/sdd.js +0 -78
- package/src/bin/generate-linked-data.ts +0 -762
- package/src/bin/process-resources-index.ts +0 -100
- package/src/bin/process-resources.ts +0 -510
- package/src/bin/util.ts +0 -74
- package/src/bin/validate-catalog.ts +0 -122
- package/src/bin/validate-resources-text.ts +0 -25
- package/src/catalog/entities.ts +0 -62
- package/src/catalog/entity.ts +0 -116
- package/src/catalog/index.ts +0 -33
- package/src/catalog/tables/author.ts +0 -15
- package/src/catalog/tables/index.ts +0 -14
- package/src/catalog/tables/place.ts +0 -14
- package/src/catalog/tables/publisher.ts +0 -15
- package/src/catalog/tables/taxon.ts +0 -17
- package/src/catalog/tables/work.ts +0 -65
- package/src/catalog/value.ts +0 -51
- package/src/csv.ts +0 -33
- package/src/index.ts +0 -4
- package/src/module.d.ts +0 -148
- package/src/resources/diff-resource.ts +0 -226
- package/src/resources/index.ts +0 -4
- package/src/resources/parse-name.ts +0 -392
- package/src/resources/parse-text.ts +0 -408
- package/src/resources/resource.ts +0 -10
- package/src/taxon-names/index.ts +0 -79
- package/test/resources.js +0 -374
- package/tsconfig.json +0 -15
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { promises as fs, existsSync as fileExists } from 'fs'
|
|
4
|
-
import * as path from 'path'
|
|
5
|
-
|
|
6
|
-
import { csv, resources } from '../index'
|
|
7
|
-
import { numericSort } from './util'
|
|
8
|
-
|
|
9
|
-
interface AmendedResourceMetadata extends ResourceMetadata {
|
|
10
|
-
id: ResourceId,
|
|
11
|
-
taxonCount: number
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
type SortObjectCallback = (a: string, b: string) => number
|
|
15
|
-
function alphabeticSort (a: string, b: string): number {
|
|
16
|
-
return a > b ? 1 : a < b ? -1 : 0
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function sortObject (object: Record<string, unknown>, sorter?: SortObjectCallback): Record<string, unknown> {
|
|
20
|
-
const sorted: Record<string, unknown> = {}
|
|
21
|
-
for (const key of Object.keys(object).sort(sorter ?? numericSort)) {
|
|
22
|
-
sorted[key] = object[key]
|
|
23
|
-
}
|
|
24
|
-
return sorted
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function addTaxon (index: Record<string, TaxonId[]>, id: string, taxon: string[]) {
|
|
28
|
-
if (!(id in index)) {
|
|
29
|
-
index[id] = []
|
|
30
|
-
}
|
|
31
|
-
index[id].push(taxon[0])
|
|
32
|
-
index[id].sort(numericSort)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function main (args: string[]): Promise<void> {
|
|
36
|
-
const REPO_ROOT = path.resolve(args[0])
|
|
37
|
-
|
|
38
|
-
const files = await fs.readdir(path.join(REPO_ROOT, 'txt'))
|
|
39
|
-
|
|
40
|
-
const gbifIndex: Record<string, TaxonId[]> = {}
|
|
41
|
-
const colIndex: Record<string, TaxonId[]> = {}
|
|
42
|
-
const resourceIndex: Record<ResourceId, AmendedResourceMetadata> = {}
|
|
43
|
-
|
|
44
|
-
await Promise.all(files.map(async function (fileName) {
|
|
45
|
-
if (!fileName.endsWith('.txt')) { return }
|
|
46
|
-
const id = fileName.slice(0, -4)
|
|
47
|
-
const file = await fs.readFile(path.join(REPO_ROOT, 'txt', fileName), 'utf-8')
|
|
48
|
-
|
|
49
|
-
return Promise.all(resources.parseTextFileHeader(file).map(async function (resource, index) {
|
|
50
|
-
const amendedResource = {
|
|
51
|
-
...resource,
|
|
52
|
-
id: `${id}:${index + 1}`,
|
|
53
|
-
taxonCount: 0
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const dwcFile = path.join(REPO_ROOT, 'dwc', `${id}-${index + 1}.csv`)
|
|
57
|
-
if (!fileExists(dwcFile)) {
|
|
58
|
-
return
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const [header, ...dwc] = csv.parseCsv(await fs.readFile(dwcFile, 'utf-8'))
|
|
62
|
-
const gbifColumn = header.indexOf('gbifTaxonID')
|
|
63
|
-
const gbifAcceptedColumn = header.indexOf('gbifAcceptedTaxonID')
|
|
64
|
-
const colColumn = header.indexOf('colTaxonID')
|
|
65
|
-
const colAcceptedColumn = header.indexOf('colAcceptedTaxonID')
|
|
66
|
-
for (const taxon of dwc) {
|
|
67
|
-
const gbifId = taxon[gbifColumn]
|
|
68
|
-
if (gbifId) {
|
|
69
|
-
addTaxon(gbifIndex, gbifId, taxon)
|
|
70
|
-
if (taxon[gbifAcceptedColumn] !== taxon[gbifColumn]) {
|
|
71
|
-
addTaxon(gbifIndex, taxon[gbifAcceptedColumn], taxon)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const colId = taxon[colColumn]
|
|
76
|
-
if (colId) {
|
|
77
|
-
addTaxon(colIndex, colId, taxon)
|
|
78
|
-
if (taxon[colAcceptedColumn] !== taxon[colColumn]) {
|
|
79
|
-
addTaxon(colIndex, taxon[colAcceptedColumn], taxon)
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
amendedResource.taxonCount += 1
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
resourceIndex[amendedResource.id] = amendedResource
|
|
87
|
-
}))
|
|
88
|
-
}))
|
|
89
|
-
|
|
90
|
-
await Promise.all([
|
|
91
|
-
fs.writeFile(path.join(REPO_ROOT, 'gbif.index.json'), JSON.stringify(sortObject(gbifIndex), null, 2)),
|
|
92
|
-
fs.writeFile(path.join(REPO_ROOT, 'col.index.json'), JSON.stringify(sortObject(colIndex, alphabeticSort), null, 2)),
|
|
93
|
-
fs.writeFile(path.join(REPO_ROOT, 'index.json'), JSON.stringify(sortObject(resourceIndex), null, 2))
|
|
94
|
-
])
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
main(process.argv.slice(2)).catch(error => {
|
|
98
|
-
console.error(error)
|
|
99
|
-
process.exit(1)
|
|
100
|
-
})
|
|
@@ -1,510 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { promises as fs, existsSync as doesFileExist } from 'fs'
|
|
4
|
-
import * as path from 'path'
|
|
5
|
-
import { spawn } from 'child_process'
|
|
6
|
-
import * as util from 'util'
|
|
7
|
-
|
|
8
|
-
import { csv } from '../index'
|
|
9
|
-
import { prompt, promptForAnswers, numericSort, runCommand } from './util'
|
|
10
|
-
|
|
11
|
-
export enum ResourceProcessorSource {
|
|
12
|
-
All = 'all',
|
|
13
|
-
Unprocessed = 'unprocessed',
|
|
14
|
-
Modified = 'modified'
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const DWC_FIELDS: (keyof AmendedTaxon)[] = [
|
|
18
|
-
'scientificNameID',
|
|
19
|
-
'scientificName',
|
|
20
|
-
'scientificNameAuthorship',
|
|
21
|
-
'genericName',
|
|
22
|
-
'infragenericEpithet',
|
|
23
|
-
'specificEpithet',
|
|
24
|
-
'infraspecificEpithet',
|
|
25
|
-
|
|
26
|
-
'taxonRank',
|
|
27
|
-
'taxonRemarks',
|
|
28
|
-
'collectionCode',
|
|
29
|
-
|
|
30
|
-
'taxonomicStatus',
|
|
31
|
-
'acceptedNameUsageID',
|
|
32
|
-
'acceptedNameUsage',
|
|
33
|
-
|
|
34
|
-
'parentNameUsageID',
|
|
35
|
-
'parentNameUsage',
|
|
36
|
-
'kingdom',
|
|
37
|
-
'phylum',
|
|
38
|
-
'class',
|
|
39
|
-
'order',
|
|
40
|
-
'family',
|
|
41
|
-
'subfamily',
|
|
42
|
-
'genus',
|
|
43
|
-
'subgenus',
|
|
44
|
-
'higherClassification',
|
|
45
|
-
'verbatimIdentification',
|
|
46
|
-
|
|
47
|
-
'dynamicProperties',
|
|
48
|
-
|
|
49
|
-
'colTaxonID',
|
|
50
|
-
'gbifTaxonID',
|
|
51
|
-
'colAcceptedTaxonID',
|
|
52
|
-
'gbifAcceptedTaxonID'
|
|
53
|
-
]
|
|
54
|
-
|
|
55
|
-
const DISPLAY_FIELDS: (keyof AmendedTaxon)[] = [
|
|
56
|
-
'scientificNameID',
|
|
57
|
-
'taxonRank',
|
|
58
|
-
'scientificName',
|
|
59
|
-
'taxonomicStatus',
|
|
60
|
-
'taxonRemarks',
|
|
61
|
-
'colTaxonID',
|
|
62
|
-
'gbifTaxonID'
|
|
63
|
-
]
|
|
64
|
-
|
|
65
|
-
const GBIF_RANKS: Rank[] = [
|
|
66
|
-
'kingdom',
|
|
67
|
-
'phyllum',
|
|
68
|
-
'class',
|
|
69
|
-
'order',
|
|
70
|
-
'family',
|
|
71
|
-
'genus',
|
|
72
|
-
'species',
|
|
73
|
-
'subspecies',
|
|
74
|
-
'variety'
|
|
75
|
-
]
|
|
76
|
-
|
|
77
|
-
function runGnverifier (names: string): Promise<string> {
|
|
78
|
-
return new Promise((resolve, reject) => {
|
|
79
|
-
const proc = spawn('gnverifier', ['-s', '1,11', '-f', 'compact', '-M'])
|
|
80
|
-
let stdout = ''
|
|
81
|
-
proc.stdout.on('data', data => { stdout += data })
|
|
82
|
-
proc.stderr.pipe(process.stdout)
|
|
83
|
-
proc.on('close', code => {
|
|
84
|
-
if (code === 0) {
|
|
85
|
-
resolve(stdout)
|
|
86
|
-
} else {
|
|
87
|
-
reject()
|
|
88
|
-
}
|
|
89
|
-
})
|
|
90
|
-
proc.stdin.write(names)
|
|
91
|
-
proc.stdin.end()
|
|
92
|
-
})
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async function listFiles (directory: string): Promise<string[]> {
|
|
96
|
-
const input = await fs.readdir(directory)
|
|
97
|
-
return input.map(file => path.basename(file, '.txt')).sort(numericSort)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async function listUnprocessedFiles (directory: string, outputDirectory: string): Promise<string[]> {
|
|
101
|
-
const input = await listFiles(directory)
|
|
102
|
-
const output = new Set(await fs.readdir(outputDirectory))
|
|
103
|
-
return input.filter(file => !output.has(file + '-1.csv'))
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async function listChangedFiles (directory: string): Promise<string[]> {
|
|
107
|
-
const output = await runCommand('git', ['diff', '--name-only', 'HEAD', '--', directory], {
|
|
108
|
-
cwd: directory
|
|
109
|
-
})
|
|
110
|
-
return output.trimEnd().split('\n').map(file => path.basename(file, '.txt')).sort(numericSort)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async function getOldFile (file: string): Promise<string> {
|
|
114
|
-
const options = {
|
|
115
|
-
cwd: path.dirname(file)
|
|
116
|
-
}
|
|
117
|
-
const gitRoot = (await runCommand('git', ['rev-parse', '--show-toplevel'], options)).trim()
|
|
118
|
-
return await runCommand('git', ['show', 'HEAD:' + path.relative(gitRoot, file)], options)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
class ResourceProcessor {
|
|
122
|
-
DIR_ROOT: string;
|
|
123
|
-
DIR_TXT: string;
|
|
124
|
-
DIR_DWC: string;
|
|
125
|
-
FILE_PROBLEMS: string;
|
|
126
|
-
|
|
127
|
-
constructor (collectionPath: string) {
|
|
128
|
-
this.DIR_ROOT = path.resolve(collectionPath)
|
|
129
|
-
this.DIR_TXT = path.join(this.DIR_ROOT, 'txt')
|
|
130
|
-
this.DIR_DWC = path.join(this.DIR_ROOT, 'dwc')
|
|
131
|
-
this.FILE_PROBLEMS = path.join(this.DIR_ROOT, 'problems.csv')
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async run (source: ResourceProcessorSource, config: ResourceProcessorConfig): Promise<void> {
|
|
135
|
-
const ids = await this.listWorks(source)
|
|
136
|
-
for (const id of ids) {
|
|
137
|
-
await this.processWork(id, config)
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async listWorks (source: ResourceProcessorSource): Promise<string[]> {
|
|
142
|
-
switch (source) {
|
|
143
|
-
case ResourceProcessorSource.All:
|
|
144
|
-
return listFiles(this.DIR_TXT)
|
|
145
|
-
case ResourceProcessorSource.Unprocessed:
|
|
146
|
-
return listUnprocessedFiles(this.DIR_TXT, this.DIR_DWC)
|
|
147
|
-
case ResourceProcessorSource.Modified:
|
|
148
|
-
return listChangedFiles(this.DIR_TXT)
|
|
149
|
-
default:
|
|
150
|
-
return []
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
async processWork (id: WorkId, config: ResourceProcessorConfig): Promise<void> {
|
|
155
|
-
const resources = await this.processResources(id, config)
|
|
156
|
-
|
|
157
|
-
await Promise.all(resources.map(resource => {
|
|
158
|
-
const header = DWC_FIELDS
|
|
159
|
-
const table: string[][] = [header]
|
|
160
|
-
|
|
161
|
-
for (const id in resource.taxa) {
|
|
162
|
-
const taxon = resource.taxa[id] as unknown as Record<string, string | undefined>
|
|
163
|
-
table.push(header.map(column => taxon[column] || ''))
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return fs.writeFile(path.join(this.DIR_DWC, `${resource.file}.csv`), csv.formatCsv(table, ',').trim())
|
|
167
|
-
}))
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async processResources (id: WorkId, config: ResourceProcessorConfig): Promise<AmendedResource[]> {
|
|
171
|
-
const resources = await this.processResourceText(id, config)
|
|
172
|
-
|
|
173
|
-
const amendedResources = []
|
|
174
|
-
for (const resource of resources) {
|
|
175
|
-
const results = await this.processResourceDwc(resource, config)
|
|
176
|
-
|
|
177
|
-
const skip = await this.shouldBeSkipped(resource.id)
|
|
178
|
-
|
|
179
|
-
if (!skip) {
|
|
180
|
-
const correct = this.checkResults(results)
|
|
181
|
-
if (!correct) {
|
|
182
|
-
const choice = await promptForAnswers(
|
|
183
|
-
`${resource.workId}: problems found in ${resource.id}. Skip or retry (s/r)? `,
|
|
184
|
-
['s', 'S', 'r', 'R']
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
switch (choice) {
|
|
188
|
-
case 's':
|
|
189
|
-
case 'S': {
|
|
190
|
-
const reason = await prompt('Reason for skipping? ')
|
|
191
|
-
fs.appendFile(this.FILE_PROBLEMS, csv.formatCsv([[
|
|
192
|
-
resource.workId,
|
|
193
|
-
resource.id,
|
|
194
|
-
reason
|
|
195
|
-
]]))
|
|
196
|
-
console.log(`${resource.workId}: skipping ${resource.id}`)
|
|
197
|
-
break
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
case 'r':
|
|
201
|
-
case 'R': {
|
|
202
|
-
console.log(`${resource.workId}: retrying ${resource.id}`)
|
|
203
|
-
return this.processResources(id, config)
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
amendedResources.push(results)
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return amendedResources
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
async processResourceText (id: WorkId, config: ResourceProcessorConfig): Promise<Resource[]> {
|
|
216
|
-
try {
|
|
217
|
-
console.log(`${id}: generating Darwin Core`)
|
|
218
|
-
const filePath = path.join(this.DIR_TXT, id + '.txt')
|
|
219
|
-
const file = await fs.readFile(filePath, 'utf-8')
|
|
220
|
-
|
|
221
|
-
let old = undefined
|
|
222
|
-
if (config.update) {
|
|
223
|
-
const dwc = []
|
|
224
|
-
for (const file of await fs.readdir(this.DIR_DWC)) {
|
|
225
|
-
if (file.startsWith(id + '-')) {
|
|
226
|
-
const filePath = path.join(this.DIR_DWC, file)
|
|
227
|
-
dwc.push(csv.parseCsv(await getOldFile(filePath)))
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
old = { txt: await getOldFile(filePath), dwc }
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const { resources } = await import('../index')
|
|
235
|
-
return resources.parseTextFile(file, id, old)
|
|
236
|
-
} catch (error) {
|
|
237
|
-
console.log(error.message)
|
|
238
|
-
await prompt(`${id}: generating Darwin Core failed, retry? `)
|
|
239
|
-
|
|
240
|
-
// Clear cache to re-import
|
|
241
|
-
const prefix = path.dirname(require.resolve('../index'))
|
|
242
|
-
for (const file in require.cache) {
|
|
243
|
-
if (file.startsWith(prefix)) {
|
|
244
|
-
delete require.cache[file]
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return this.processResourceText(id, config)
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
async processResourceDwc (resource: Resource, config: ResourceProcessorConfig): Promise<AmendedResource> {
|
|
253
|
-
console.log(`${resource.workId}: matching ${resource.id}`)
|
|
254
|
-
|
|
255
|
-
if (!config.updateMappings) {
|
|
256
|
-
const file = path.join(this.DIR_DWC, resource.file + '.csv')
|
|
257
|
-
if (doesFileExist(file)) {
|
|
258
|
-
const [header, ...rows] = csv.parseCsv(await fs.readFile(file, 'utf-8'))
|
|
259
|
-
for (const row of rows) {
|
|
260
|
-
const oldTaxon = row.reduce((taxon, value, index) => {
|
|
261
|
-
taxon[header[index]] = value
|
|
262
|
-
return taxon
|
|
263
|
-
}, {} as Record<string, string>)
|
|
264
|
-
const taxon = resource.taxa[oldTaxon.scientificNameID] as AmendedTaxon
|
|
265
|
-
if (taxon) {
|
|
266
|
-
taxon.colTaxonID = oldTaxon.colTaxonID
|
|
267
|
-
taxon.colAcceptedTaxonID = oldTaxon.colAcceptedTaxonID
|
|
268
|
-
taxon.gbifTaxonID = oldTaxon.gbifTaxonID
|
|
269
|
-
taxon.gbifAcceptedTaxonID = oldTaxon.gbifAcceptedTaxonID
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
return resource as AmendedResource
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const filteredResults: Record<TaxonId, TaxonMatch[]> = {}
|
|
277
|
-
const taxonNames: Record<string, TaxonId[]> = {}
|
|
278
|
-
const names = new Set()
|
|
279
|
-
for (const id in resource.taxa) {
|
|
280
|
-
const name = resource.taxa[id].scientificName
|
|
281
|
-
|
|
282
|
-
if (!taxonNames[name]) { taxonNames[name] = [] }
|
|
283
|
-
taxonNames[name].push(id)
|
|
284
|
-
|
|
285
|
-
names.add(name)
|
|
286
|
-
filteredResults[id] = []
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const result = await runGnverifier(Array.from(names).join('\n'))
|
|
290
|
-
for (const results of result.trim().split('\n')) {
|
|
291
|
-
interface MatchScoreDetails {
|
|
292
|
-
cardinalityScore: number;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
interface Match {
|
|
296
|
-
currentRecordId: string;
|
|
297
|
-
dataSourceId: number;
|
|
298
|
-
matchedName: string;
|
|
299
|
-
recordId: string;
|
|
300
|
-
sortScore: number;
|
|
301
|
-
isSynonym: boolean;
|
|
302
|
-
classificationPath: string;
|
|
303
|
-
classificationRanks: string;
|
|
304
|
-
scoreDetails: MatchScoreDetails;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const { name, results: matches } = JSON.parse(results)
|
|
308
|
-
|
|
309
|
-
if (!matches) {
|
|
310
|
-
continue
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Fix author scoring for some species, see https://github.com/gnames/gnverifier/issues/129
|
|
314
|
-
matches.sort((a: Match, b: Match) => {
|
|
315
|
-
if (a.sortScore !== b.sortScore) {
|
|
316
|
-
return b.sortScore - a.sortScore
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
return name === a.matchedName ? -1 : name === b.matchedName ? 1 : 0
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
for (const match of matches as Match[]) {
|
|
323
|
-
const source = match.dataSourceId
|
|
324
|
-
const currentRank = match.classificationRanks.split('|').pop()
|
|
325
|
-
|
|
326
|
-
if (match.scoreDetails.cardinalityScore === 0) {
|
|
327
|
-
// Rank mismatch
|
|
328
|
-
continue
|
|
329
|
-
} else if (source === 11 && currentRank === 'species' && match.classificationPath.endsWith(' spec')) {
|
|
330
|
-
// GBIF species like "Nomada spec"
|
|
331
|
-
continue
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
for (const loirId of taxonNames[name]) {
|
|
335
|
-
const taxon = resource.taxa[loirId]
|
|
336
|
-
|
|
337
|
-
if (source === 11 && !GBIF_RANKS.includes(taxon.taxonRank)) {
|
|
338
|
-
// Exclude GBIF matches for ranks that are not in GBIF
|
|
339
|
-
continue
|
|
340
|
-
} else if (source === 11 && !match.isSynonym && currentRank !== taxon.taxonRank) {
|
|
341
|
-
// Exclude matches with rank mismatches (only possible
|
|
342
|
-
// for non-synonyms).
|
|
343
|
-
continue
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (!filteredResults[loirId]) {
|
|
347
|
-
filteredResults[loirId] = []
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
filteredResults[loirId].push({
|
|
351
|
-
source,
|
|
352
|
-
id: match.recordId,
|
|
353
|
-
currentId: match.currentRecordId,
|
|
354
|
-
classificationPath: match.classificationPath.split('|')
|
|
355
|
-
})
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const { taxonNames: { amendResource, groupNameMatches } } = await import('../index')
|
|
361
|
-
const groupedNameMatches = groupNameMatches(filteredResults)
|
|
362
|
-
|
|
363
|
-
const amendedResource: AmendedResource = { ...resource, taxa: { ...resource.taxa } }
|
|
364
|
-
for (const source in groupedNameMatches) {
|
|
365
|
-
const matches = await this.selectPrefixes(resource, groupedNameMatches, source)
|
|
366
|
-
amendResource(amendedResource, source, matches)
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
return amendedResource
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
async selectPrefixes (resource: Resource, groupedNameMatches: GroupedNameMatches, source: string): Promise<Record<TaxonId, TaxonMatch>> {
|
|
373
|
-
const prefixes = Object.keys(groupedNameMatches[source])
|
|
374
|
-
if (prefixes.length === 0) {
|
|
375
|
-
return {}
|
|
376
|
-
} else if (prefixes.length === 1) {
|
|
377
|
-
return groupedNameMatches[source][prefixes[0]]
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Count total mapped taxa
|
|
381
|
-
const mappedTaxa: Record<TaxonId, boolean> = {}
|
|
382
|
-
for (const prefix of prefixes) {
|
|
383
|
-
for (const taxon in groupedNameMatches[source][prefix]) {
|
|
384
|
-
mappedTaxa[taxon] = true
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
const missedTaxonCount = Object.keys(mappedTaxa).length - Object.keys(groupedNameMatches[source][prefixes[0]]).length
|
|
388
|
-
|
|
389
|
-
if (missedTaxonCount === 0) {
|
|
390
|
-
// Multiple prefixes but the first one maps all taxa (not counting that are unmapped in all prefixes)
|
|
391
|
-
return groupedNameMatches[source][prefixes[0]]
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
console.error(`${resource.workId}: source ${source} results in multiple prefixes`)
|
|
395
|
-
|
|
396
|
-
let choice
|
|
397
|
-
if (missedTaxonCount <= 5) {
|
|
398
|
-
console.error(` Most common prefix misses ${missedTaxonCount} taxa: automatically selecting most common prefix...`)
|
|
399
|
-
choice = '1'
|
|
400
|
-
} else if (source === '1') {
|
|
401
|
-
console.error(` Catalogue of Life: automatically selecting most common prefix...`)
|
|
402
|
-
choice = '1'
|
|
403
|
-
} else {
|
|
404
|
-
for (let i = 0; i < prefixes.length; i++) {
|
|
405
|
-
const prefix = prefixes[i]
|
|
406
|
-
const taxa = groupedNameMatches[source][prefix]
|
|
407
|
-
const taxonIds = Object.keys(taxa)
|
|
408
|
-
|
|
409
|
-
console.error(` [${i + 1}] ${prefix} (${taxonIds.length} taxa)`)
|
|
410
|
-
for (let j = 0; j < Math.min(9, taxonIds.length); j++) {
|
|
411
|
-
const taxonId = taxonIds[j]
|
|
412
|
-
const taxon = resource.taxa[taxonId]
|
|
413
|
-
const match = taxa[taxonId]
|
|
414
|
-
console.error(` taxon: ${taxonId} "${taxon.scientificName}" - ${match.classificationPath.join('|')}`)
|
|
415
|
-
}
|
|
416
|
-
if (taxonIds.length > 9) {
|
|
417
|
-
console.error(` ...`)
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
do {
|
|
422
|
-
choice = await prompt(` Select prefixes (1-${prefixes.length})? `)
|
|
423
|
-
} while (!/^(|\d+(,\d+)*)$/.test(choice))
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
console.error(` Applying selection...`)
|
|
427
|
-
|
|
428
|
-
if (choice === '') {
|
|
429
|
-
return {}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const matches: Record<TaxonId, TaxonMatch> = {}
|
|
433
|
-
for (const i of choice.split(',')) {
|
|
434
|
-
const prefix = prefixes[parseInt(i) - 1]
|
|
435
|
-
const taxa = groupedNameMatches[source][prefix]
|
|
436
|
-
for (const id in taxa) {
|
|
437
|
-
if (id in matches) {
|
|
438
|
-
continue
|
|
439
|
-
}
|
|
440
|
-
matches[id] = taxa[id]
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
return matches
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
checkResults (resource: AmendedResource): boolean {
|
|
448
|
-
let correct = true
|
|
449
|
-
const missing = []
|
|
450
|
-
|
|
451
|
-
for (const id in resource.taxa) {
|
|
452
|
-
const taxon = resource.taxa[id]
|
|
453
|
-
if (taxon.taxonomicStatus !== 'accepted') { continue }
|
|
454
|
-
|
|
455
|
-
const missingCol = false // !taxon.colTaxonID
|
|
456
|
-
const missingGbif = GBIF_RANKS.includes(taxon.taxonRank) && !taxon.gbifTaxonID
|
|
457
|
-
|
|
458
|
-
if (missingCol || missingGbif) {
|
|
459
|
-
correct = false
|
|
460
|
-
missing.push(taxon)
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
if (missing.length) {
|
|
465
|
-
console.table(missing, DISPLAY_FIELDS)
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
return correct
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
async shouldBeSkipped (id: ResourceId): Promise<boolean> {
|
|
472
|
-
const problems = csv.parseCsv(await fs.readFile(this.FILE_PROBLEMS, 'utf8'))
|
|
473
|
-
return problems.some(([_work, resource, _problem]) => resource === id)
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function main (): void {
|
|
478
|
-
const args = util.parseArgs({
|
|
479
|
-
options: {
|
|
480
|
-
source: {
|
|
481
|
-
type: 'string',
|
|
482
|
-
short: 's',
|
|
483
|
-
default: 'unprocessed'
|
|
484
|
-
},
|
|
485
|
-
'keep-mappings': {
|
|
486
|
-
type: 'boolean',
|
|
487
|
-
short: 'k'
|
|
488
|
-
}
|
|
489
|
-
},
|
|
490
|
-
allowPositionals: true
|
|
491
|
-
})
|
|
492
|
-
|
|
493
|
-
const processor = new ResourceProcessor(args.positionals[0])
|
|
494
|
-
process.on('exit', () => {
|
|
495
|
-
process.stdout.write('\n')
|
|
496
|
-
})
|
|
497
|
-
|
|
498
|
-
const source = args.values.source as ResourceProcessorSource
|
|
499
|
-
const config: ResourceProcessorConfig = {
|
|
500
|
-
update: source !== 'unprocessed',
|
|
501
|
-
updateMappings: !args.values['keep-mappings']
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
processor.run(source, config).catch(error => {
|
|
505
|
-
console.error(error)
|
|
506
|
-
process.exit(1)
|
|
507
|
-
})
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
main()
|
package/src/bin/util.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import * as readline from 'readline'
|
|
2
|
-
import { spawn } from 'child_process'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Comparison function to sort strings with numerical components.
|
|
6
|
-
*/
|
|
7
|
-
export function numericSort (a: string, b: string): number {
|
|
8
|
-
const as = a.split(/(\d+)/)
|
|
9
|
-
const bs = b.split(/(\d+)/)
|
|
10
|
-
for (let i = 0; i < Math.max(as.length, bs.length); i++) {
|
|
11
|
-
const ai = as[i]
|
|
12
|
-
const bi = bs[i]
|
|
13
|
-
|
|
14
|
-
if (ai === bi) {
|
|
15
|
-
continue
|
|
16
|
-
} else if (!ai) {
|
|
17
|
-
return 1
|
|
18
|
-
} else if (!bi) {
|
|
19
|
-
return -1
|
|
20
|
-
} else if (i % 2) {
|
|
21
|
-
return parseInt(ai) - parseInt(bi)
|
|
22
|
-
} else {
|
|
23
|
-
return ai > bi ? -1 : 1
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return 0
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Create a CLI prompt and return any answer.
|
|
31
|
-
*/
|
|
32
|
-
export function prompt (question: string): Promise<string> {
|
|
33
|
-
const rl = readline.createInterface({
|
|
34
|
-
input: process.stdin,
|
|
35
|
-
output: process.stdout
|
|
36
|
-
})
|
|
37
|
-
return new Promise(resolve => {
|
|
38
|
-
rl.question(question, (answer: string) => {
|
|
39
|
-
rl.close()
|
|
40
|
-
resolve(answer)
|
|
41
|
-
})
|
|
42
|
-
})
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Create a CLI prompt and only accept certain answers.
|
|
47
|
-
*/
|
|
48
|
-
export async function promptForAnswers (question: string, answers: string[]) {
|
|
49
|
-
let answer
|
|
50
|
-
|
|
51
|
-
do {
|
|
52
|
-
answer = (await prompt(question))[0]
|
|
53
|
-
} while (!answers.includes(answer))
|
|
54
|
-
|
|
55
|
-
return answer
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Run a command and return stdout.
|
|
60
|
-
*/
|
|
61
|
-
export function runCommand (command: string, args: string[], options?: Record<string, unknown>): Promise<string> {
|
|
62
|
-
return new Promise((resolve, reject) => {
|
|
63
|
-
const proc = spawn(command, args, options)
|
|
64
|
-
let stdout = ''
|
|
65
|
-
proc.stdout.on('data', data => { stdout += data })
|
|
66
|
-
proc.on('close', code => {
|
|
67
|
-
if (code === 0) {
|
|
68
|
-
resolve(stdout)
|
|
69
|
-
} else {
|
|
70
|
-
reject()
|
|
71
|
-
}
|
|
72
|
-
})
|
|
73
|
-
})
|
|
74
|
-
}
|