@larsgw/formica 0.2.1 → 0.3.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/CHANGELOG.md +13 -0
- package/lib/bin/process-resources-index.js +10 -4
- package/lib/bin/process-resources.js +214 -142
- package/lib/index.d.ts +1 -0
- package/lib/index.js +2 -1
- package/lib/taxon-names/index.d.ts +2 -0
- package/lib/taxon-names/index.js +79 -0
- package/package.json +1 -1
- package/src/bin/process-resources-index.ts +11 -4
- package/src/bin/process-resources.ts +154 -99
- package/src/index.ts +1 -0
- package/src/module.d.ts +70 -50
- package/src/taxon-names/index.ts +79 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
# [0.3.0](https://github.com/identification-resources/formica/compare/v0.2.1...v0.3.0) (2023-08-20)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **resources:** add accepted ids to dwc and index ([1b5aba8](https://github.com/identification-resources/formica/commit/1b5aba8ff07bba32fc1bbf49927fef7a880eb35e))
|
|
7
|
+
* **resources:** allow updates of just mappings ([0ea62c1](https://github.com/identification-resources/formica/commit/0ea62c143a49129709960de123c0037973356487))
|
|
8
|
+
* **resources:** improve prefix selection heuristics ([7c2e0c9](https://github.com/identification-resources/formica/commit/7c2e0c99d9de7490314f971e8b4e9e5e190a81c2))
|
|
9
|
+
* **resources:** improve taxon name matching ([78ff480](https://github.com/identification-resources/formica/commit/78ff480485ca42bd1d1f2893f230e61db6cb6be8)), closes [#2](https://github.com/identification-resources/formica/issues/2)
|
|
10
|
+
* **resources:** test for rank mismatch ([0e69b7f](https://github.com/identification-resources/formica/commit/0e69b7f0a6654fa5255e797be7516cd06edb4df3)), closes [#2](https://github.com/identification-resources/formica/issues/2)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
1
14
|
## [0.2.1](https://github.com/identification-resources/formica/compare/v0.2.0...v0.2.1) (2023-08-10)
|
|
2
15
|
|
|
3
16
|
|
|
@@ -62,6 +62,13 @@ function sortObject(object) {
|
|
|
62
62
|
return sorted;
|
|
63
63
|
}
|
|
64
64
|
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
65
|
+
function addTaxon(gbifIndex, gbifId, taxon) {
|
|
66
|
+
if (!(gbifId in gbifIndex)) {
|
|
67
|
+
gbifIndex[gbifId] = [];
|
|
68
|
+
}
|
|
69
|
+
gbifIndex[gbifId].push(taxon[0]);
|
|
70
|
+
gbifIndex[gbifId].sort(util_1.numericSort);
|
|
71
|
+
}
|
|
65
72
|
function main(args) {
|
|
66
73
|
return __awaiter(this, void 0, void 0, function () {
|
|
67
74
|
var REPO_ROOT, files, gbifIndex, resourceIndex;
|
|
@@ -106,11 +113,10 @@ function main(args) {
|
|
|
106
113
|
taxon = dwc_1[_i];
|
|
107
114
|
gbifId = taxon[25];
|
|
108
115
|
if (gbifId) {
|
|
109
|
-
|
|
110
|
-
|
|
116
|
+
addTaxon(gbifIndex, gbifId, taxon);
|
|
117
|
+
if (taxon[27] !== taxon[25]) {
|
|
118
|
+
addTaxon(gbifIndex, taxon[27], taxon);
|
|
111
119
|
}
|
|
112
|
-
gbifIndex[gbifId].push(taxon[0]);
|
|
113
|
-
gbifIndex[gbifId].sort(util_1.numericSort);
|
|
114
120
|
}
|
|
115
121
|
amendedResource.taxonCount += 1;
|
|
116
122
|
}
|
|
@@ -80,7 +80,9 @@ var DWC_FIELDS = [
|
|
|
80
80
|
'subgenus',
|
|
81
81
|
'higherClassification',
|
|
82
82
|
'colTaxonID',
|
|
83
|
-
'gbifTaxonID'
|
|
83
|
+
'gbifTaxonID',
|
|
84
|
+
'colAcceptedTaxonID',
|
|
85
|
+
'gbifAcceptedTaxonID'
|
|
84
86
|
];
|
|
85
87
|
var DISPLAY_FIELDS = [
|
|
86
88
|
'scientificNameID',
|
|
@@ -102,16 +104,9 @@ var GBIF_RANKS = [
|
|
|
102
104
|
'subspecies',
|
|
103
105
|
'variety'
|
|
104
106
|
];
|
|
105
|
-
var VALID_COMMON_PREFIXES = [
|
|
106
|
-
'Plantae|Tracheophyta',
|
|
107
|
-
'Fungi',
|
|
108
|
-
'Fungi|Ascomycota',
|
|
109
|
-
'Fungi|Basidiomycota',
|
|
110
|
-
'Fungi|Zygomycota'
|
|
111
|
-
];
|
|
112
107
|
function runGnverifier(names) {
|
|
113
108
|
return new Promise(function (resolve, reject) {
|
|
114
|
-
var proc = (0, child_process_1.spawn)('gnverifier', ['-s', '1,11', '-M']);
|
|
109
|
+
var proc = (0, child_process_1.spawn)('gnverifier', ['-s', '1,11', '-f', 'compact', '-M']);
|
|
115
110
|
var stdout = '';
|
|
116
111
|
proc.stdout.on('data', function (data) { stdout += data; });
|
|
117
112
|
proc.stderr.pipe(process.stdout);
|
|
@@ -241,6 +236,34 @@ var ResourceProcessor = /** @class */ (function () {
|
|
|
241
236
|
});
|
|
242
237
|
});
|
|
243
238
|
};
|
|
239
|
+
ResourceProcessor.prototype.runMappingsUpdate = function () {
|
|
240
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
241
|
+
var input, ids, _i, ids_2, id;
|
|
242
|
+
return __generator(this, function (_a) {
|
|
243
|
+
switch (_a.label) {
|
|
244
|
+
case 0: return [4 /*yield*/, fs_1.promises.readdir(this.DIR_TXT)];
|
|
245
|
+
case 1:
|
|
246
|
+
input = _a.sent();
|
|
247
|
+
ids = input
|
|
248
|
+
.map(function (file) { return path.basename(file, '.txt'); })
|
|
249
|
+
.sort(function (a, b) { return parseInt(a.slice(1)) - parseInt(b.slice(1)); });
|
|
250
|
+
_i = 0, ids_2 = ids;
|
|
251
|
+
_a.label = 2;
|
|
252
|
+
case 2:
|
|
253
|
+
if (!(_i < ids_2.length)) return [3 /*break*/, 5];
|
|
254
|
+
id = ids_2[_i];
|
|
255
|
+
return [4 /*yield*/, this.processWork(id, true)];
|
|
256
|
+
case 3:
|
|
257
|
+
_a.sent();
|
|
258
|
+
_a.label = 4;
|
|
259
|
+
case 4:
|
|
260
|
+
_i++;
|
|
261
|
+
return [3 /*break*/, 2];
|
|
262
|
+
case 5: return [2 /*return*/];
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
};
|
|
244
267
|
ResourceProcessor.prototype.processWork = function (id, update) {
|
|
245
268
|
return __awaiter(this, void 0, void 0, function () {
|
|
246
269
|
var resources;
|
|
@@ -271,79 +294,61 @@ var ResourceProcessor = /** @class */ (function () {
|
|
|
271
294
|
};
|
|
272
295
|
ResourceProcessor.prototype.processResources = function (id, update) {
|
|
273
296
|
return __awaiter(this, void 0, void 0, function () {
|
|
274
|
-
var resources, amendedResources, _i, resources_1, resource,
|
|
275
|
-
return __generator(this, function (
|
|
276
|
-
switch (
|
|
297
|
+
var resources, amendedResources, _i, resources_1, resource, results, skip, correct, choice, _a, reason;
|
|
298
|
+
return __generator(this, function (_b) {
|
|
299
|
+
switch (_b.label) {
|
|
277
300
|
case 0: return [4 /*yield*/, this.processResourceText(id, update)];
|
|
278
301
|
case 1:
|
|
279
|
-
resources =
|
|
302
|
+
resources = _b.sent();
|
|
280
303
|
amendedResources = [];
|
|
281
304
|
_i = 0, resources_1 = resources;
|
|
282
|
-
|
|
305
|
+
_b.label = 2;
|
|
283
306
|
case 2:
|
|
284
|
-
if (!(_i < resources_1.length)) return [3 /*break*/,
|
|
307
|
+
if (!(_i < resources_1.length)) return [3 /*break*/, 11];
|
|
285
308
|
resource = resources_1[_i];
|
|
286
309
|
return [4 /*yield*/, this.processResourceDwc(resource)];
|
|
287
310
|
case 3:
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
_c = [];
|
|
291
|
-
for (_d in _b)
|
|
292
|
-
_c.push(_d);
|
|
293
|
-
_e = 0;
|
|
294
|
-
_g.label = 4;
|
|
311
|
+
results = _b.sent();
|
|
312
|
+
return [4 /*yield*/, this.shouldBeSkipped(resource.id)];
|
|
295
313
|
case 4:
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (!(_d in _b)) return [3 /*break*/, 6];
|
|
299
|
-
source = _d;
|
|
300
|
-
return [4 /*yield*/, this.checkPrefix(resource, classifications, source)];
|
|
301
|
-
case 5:
|
|
302
|
-
_g.sent();
|
|
303
|
-
_g.label = 6;
|
|
304
|
-
case 6:
|
|
305
|
-
_e++;
|
|
306
|
-
return [3 /*break*/, 4];
|
|
307
|
-
case 7: return [4 /*yield*/, this.shouldBeSkipped(resource.id)];
|
|
308
|
-
case 8:
|
|
309
|
-
skip = _g.sent();
|
|
310
|
-
if (!!skip) return [3 /*break*/, 13];
|
|
314
|
+
skip = _b.sent();
|
|
315
|
+
if (!!skip) return [3 /*break*/, 9];
|
|
311
316
|
correct = this.checkResults(results);
|
|
312
|
-
if (!!correct) return [3 /*break*/,
|
|
317
|
+
if (!!correct) return [3 /*break*/, 9];
|
|
313
318
|
return [4 /*yield*/, (0, util_1.promptForAnswers)("".concat(resource.workId, ": problems found in ").concat(resource.id, ". Skip or retry (s/r)? "), ['s', 'S', 'r', 'R'])];
|
|
314
|
-
case
|
|
315
|
-
choice =
|
|
316
|
-
|
|
317
|
-
switch (
|
|
318
|
-
case 's': return [3 /*break*/,
|
|
319
|
-
case 'S': return [3 /*break*/,
|
|
320
|
-
case 'r': return [3 /*break*/,
|
|
321
|
-
case 'R': return [3 /*break*/,
|
|
319
|
+
case 5:
|
|
320
|
+
choice = _b.sent();
|
|
321
|
+
_a = choice;
|
|
322
|
+
switch (_a) {
|
|
323
|
+
case 's': return [3 /*break*/, 6];
|
|
324
|
+
case 'S': return [3 /*break*/, 6];
|
|
325
|
+
case 'r': return [3 /*break*/, 8];
|
|
326
|
+
case 'R': return [3 /*break*/, 8];
|
|
322
327
|
}
|
|
323
|
-
return [3 /*break*/,
|
|
324
|
-
case
|
|
325
|
-
case
|
|
326
|
-
reason =
|
|
328
|
+
return [3 /*break*/, 9];
|
|
329
|
+
case 6: return [4 /*yield*/, (0, util_1.prompt)('Reason for skipping? ')];
|
|
330
|
+
case 7:
|
|
331
|
+
reason = _b.sent();
|
|
327
332
|
fs_1.promises.appendFile(this.FILE_PROBLEMS, index_1.csv.formatCsv([[
|
|
328
333
|
resource.workId,
|
|
329
334
|
resource.id,
|
|
330
335
|
reason
|
|
331
336
|
]]));
|
|
332
337
|
console.log("".concat(resource.workId, ": skipping ").concat(resource.id));
|
|
333
|
-
return [3 /*break*/,
|
|
334
|
-
case
|
|
338
|
+
return [3 /*break*/, 9];
|
|
339
|
+
case 8:
|
|
335
340
|
{
|
|
336
341
|
console.log("".concat(resource.workId, ": retrying ").concat(resource.id));
|
|
337
342
|
return [2 /*return*/, this.processResources(id, update)];
|
|
338
343
|
}
|
|
339
|
-
|
|
340
|
-
case
|
|
344
|
+
_b.label = 9;
|
|
345
|
+
case 9:
|
|
341
346
|
amendedResources.push(results);
|
|
342
|
-
|
|
343
|
-
case
|
|
347
|
+
_b.label = 10;
|
|
348
|
+
case 10:
|
|
344
349
|
_i++;
|
|
345
350
|
return [3 /*break*/, 2];
|
|
346
|
-
case
|
|
351
|
+
case 11: return [2 /*return*/, amendedResources];
|
|
347
352
|
}
|
|
348
353
|
});
|
|
349
354
|
});
|
|
@@ -415,119 +420,174 @@ var ResourceProcessor = /** @class */ (function () {
|
|
|
415
420
|
};
|
|
416
421
|
ResourceProcessor.prototype.processResourceDwc = function (resource) {
|
|
417
422
|
return __awaiter(this, void 0, void 0, function () {
|
|
418
|
-
var
|
|
419
|
-
return __generator(this, function (
|
|
420
|
-
switch (
|
|
423
|
+
var filteredResults, taxonNames, names, id, name_1, result, _i, _a, results, _b, name_2, matches, _c, matches_1, match, source, currentRank, _d, _e, loirId, taxon, _f, amendResource, groupNameMatches, groupedNameMatches, amendedResource, _g, _h, _j, _k, source, matches;
|
|
424
|
+
return __generator(this, function (_l) {
|
|
425
|
+
switch (_l.label) {
|
|
421
426
|
case 0:
|
|
422
427
|
console.log("".concat(resource.workId, ": matching ").concat(resource.id));
|
|
423
|
-
|
|
424
|
-
|
|
428
|
+
filteredResults = {};
|
|
429
|
+
taxonNames = {};
|
|
430
|
+
names = new Set();
|
|
425
431
|
for (id in resource.taxa) {
|
|
426
432
|
name_1 = resource.taxa[id].scientificName;
|
|
427
|
-
if (!
|
|
428
|
-
|
|
433
|
+
if (!taxonNames[name_1]) {
|
|
434
|
+
taxonNames[name_1] = [];
|
|
429
435
|
}
|
|
430
|
-
|
|
431
|
-
names.
|
|
436
|
+
taxonNames[name_1].push(id);
|
|
437
|
+
names.add(name_1);
|
|
438
|
+
filteredResults[id] = [];
|
|
432
439
|
}
|
|
433
|
-
return [4 /*yield*/, runGnverifier(names.join('\n'))];
|
|
440
|
+
return [4 /*yield*/, runGnverifier(Array.from(names).join('\n'))];
|
|
434
441
|
case 1:
|
|
435
|
-
result =
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if (
|
|
447
|
-
|
|
448
|
-
|
|
442
|
+
result = _l.sent();
|
|
443
|
+
for (_i = 0, _a = result.trim().split('\n'); _i < _a.length; _i++) {
|
|
444
|
+
results = _a[_i];
|
|
445
|
+
_b = JSON.parse(results), name_2 = _b.name, matches = _b.results;
|
|
446
|
+
if (!matches) {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
for (_c = 0, matches_1 = matches; _c < matches_1.length; _c++) {
|
|
450
|
+
match = matches_1[_c];
|
|
451
|
+
source = match.dataSourceId;
|
|
452
|
+
currentRank = match.classificationRanks.split('|').pop();
|
|
453
|
+
if (match.scoreDetails.cardinalityScore === 0) {
|
|
454
|
+
// Rank mismatch
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
else if (source === 11 && currentRank === 'species' && match.classificationPath.endsWith(' spec')) {
|
|
458
|
+
// GBIF species like "Nomada spec"
|
|
459
|
+
continue;
|
|
449
460
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
461
|
+
for (_d = 0, _e = taxonNames[name_2]; _d < _e.length; _d++) {
|
|
462
|
+
loirId = _e[_d];
|
|
463
|
+
taxon = resource.taxa[loirId];
|
|
464
|
+
if (source === 11 && !GBIF_RANKS.includes(taxon.taxonRank)) {
|
|
465
|
+
// Exclude GBIF matches for ranks that are not in GBIF
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
else if (source === 11 && !match.isSynonym && currentRank !== taxon.taxonRank) {
|
|
469
|
+
// Exclude matches with rank mismatches (only possible
|
|
470
|
+
// for non-synonyms).
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (!filteredResults[loirId]) {
|
|
474
|
+
filteredResults[loirId] = [];
|
|
475
|
+
}
|
|
476
|
+
filteredResults[loirId].push({
|
|
477
|
+
source: source,
|
|
478
|
+
id: match.recordId,
|
|
479
|
+
currentId: match.currentRecordId,
|
|
480
|
+
classificationPath: match.classificationPath.split('|')
|
|
481
|
+
});
|
|
453
482
|
}
|
|
454
483
|
}
|
|
455
484
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
485
|
+
return [4 /*yield*/, Promise.resolve().then(function () { return require('../index'); })];
|
|
486
|
+
case 2:
|
|
487
|
+
_f = (_l.sent()).taxonNames, amendResource = _f.amendResource, groupNameMatches = _f.groupNameMatches;
|
|
488
|
+
groupedNameMatches = groupNameMatches(filteredResults);
|
|
489
|
+
amendedResource = __assign(__assign({}, resource), { taxa: __assign({}, resource.taxa) });
|
|
490
|
+
_g = groupedNameMatches;
|
|
491
|
+
_h = [];
|
|
492
|
+
for (_j in _g)
|
|
493
|
+
_h.push(_j);
|
|
494
|
+
_k = 0;
|
|
495
|
+
_l.label = 3;
|
|
496
|
+
case 3:
|
|
497
|
+
if (!(_k < _h.length)) return [3 /*break*/, 6];
|
|
498
|
+
_j = _h[_k];
|
|
499
|
+
if (!(_j in _g)) return [3 /*break*/, 5];
|
|
500
|
+
source = _j;
|
|
501
|
+
return [4 /*yield*/, this.selectPrefixes(resource, groupedNameMatches, source)];
|
|
502
|
+
case 4:
|
|
503
|
+
matches = _l.sent();
|
|
504
|
+
amendResource(amendedResource, source, matches);
|
|
505
|
+
_l.label = 5;
|
|
506
|
+
case 5:
|
|
507
|
+
_k++;
|
|
508
|
+
return [3 /*break*/, 3];
|
|
509
|
+
case 6: return [2 /*return*/, amendedResource];
|
|
461
510
|
}
|
|
462
511
|
});
|
|
463
512
|
});
|
|
464
513
|
};
|
|
465
|
-
ResourceProcessor.prototype.
|
|
514
|
+
ResourceProcessor.prototype.selectPrefixes = function (resource, groupedNameMatches, source) {
|
|
466
515
|
return __awaiter(this, void 0, void 0, function () {
|
|
467
|
-
var
|
|
516
|
+
var prefixes, mappedTaxa, _i, prefixes_1, prefix, taxon, missedTaxonCount, choice, i, prefix, taxa, taxonIds, j, taxonId, taxon, match, matches, _a, _b, i, prefix, taxa, id;
|
|
468
517
|
return __generator(this, function (_c) {
|
|
469
518
|
switch (_c.label) {
|
|
470
519
|
case 0:
|
|
471
|
-
|
|
472
|
-
if (
|
|
473
|
-
return [2 /*return
|
|
520
|
+
prefixes = Object.keys(groupedNameMatches[source]);
|
|
521
|
+
if (prefixes.length === 0) {
|
|
522
|
+
return [2 /*return*/, {}];
|
|
474
523
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
if (
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
524
|
+
else if (prefixes.length === 1) {
|
|
525
|
+
return [2 /*return*/, groupedNameMatches[source][prefixes[0]]];
|
|
526
|
+
}
|
|
527
|
+
mappedTaxa = {};
|
|
528
|
+
for (_i = 0, prefixes_1 = prefixes; _i < prefixes_1.length; _i++) {
|
|
529
|
+
prefix = prefixes_1[_i];
|
|
530
|
+
for (taxon in groupedNameMatches[source][prefix]) {
|
|
531
|
+
mappedTaxa[taxon] = true;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
missedTaxonCount = Object.keys(mappedTaxa).length - Object.keys(groupedNameMatches[source][prefixes[0]]).length;
|
|
535
|
+
if (missedTaxonCount === 0) {
|
|
536
|
+
// Multiple prefixes but the first one maps all taxa (not counting that are unmapped in all prefixes)
|
|
537
|
+
return [2 /*return*/, groupedNameMatches[source][prefixes[0]]];
|
|
538
|
+
}
|
|
539
|
+
console.error("".concat(resource.workId, ": source ").concat(source, " results in multiple prefixes"));
|
|
540
|
+
if (!(missedTaxonCount <= 5)) return [3 /*break*/, 1];
|
|
541
|
+
console.error(" Most common prefix misses ".concat(missedTaxonCount, " taxa: automatically selecting most common prefix..."));
|
|
542
|
+
choice = '1';
|
|
490
543
|
return [3 /*break*/, 6];
|
|
491
|
-
case
|
|
492
|
-
if (!
|
|
493
|
-
|
|
544
|
+
case 1:
|
|
545
|
+
if (!(source === '1')) return [3 /*break*/, 2];
|
|
546
|
+
console.error(" Catalogue of Life: automatically selecting most common prefix...");
|
|
547
|
+
choice = '1';
|
|
494
548
|
return [3 /*break*/, 6];
|
|
549
|
+
case 2:
|
|
550
|
+
for (i = 0; i < prefixes.length; i++) {
|
|
551
|
+
prefix = prefixes[i];
|
|
552
|
+
taxa = groupedNameMatches[source][prefix];
|
|
553
|
+
taxonIds = Object.keys(taxa);
|
|
554
|
+
console.error(" [".concat(i + 1, "] ").concat(prefix, " (").concat(taxonIds.length, " taxa)"));
|
|
555
|
+
for (j = 0; j < Math.min(9, taxonIds.length); j++) {
|
|
556
|
+
taxonId = taxonIds[j];
|
|
557
|
+
taxon = resource.taxa[taxonId];
|
|
558
|
+
match = taxa[taxonId];
|
|
559
|
+
console.error(" taxon: ".concat(taxonId, " \"").concat(taxon.scientificName, "\" - ").concat(match.classificationPath.join('|')));
|
|
560
|
+
}
|
|
561
|
+
if (taxonIds.length > 9) {
|
|
562
|
+
console.error(" ...");
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
_c.label = 3;
|
|
566
|
+
case 3: return [4 /*yield*/, (0, util_1.prompt)(" Select prefixes (1-".concat(prefixes.length, ")? "))];
|
|
495
567
|
case 4:
|
|
496
|
-
console.log("".concat(resource.workId, ": source ").concat(source, " results in short prefix \"").concat(prefix.slice(0, i).join('|'), "\" (").concat(i, " taxa)"));
|
|
497
|
-
console.log(" taxon: ".concat(taxon.scientificNameID, " \"").concat(taxon.scientificName, "\""));
|
|
498
|
-
console.log(" class: ".concat(parts.join('|')));
|
|
499
|
-
console.log(" prefx: ".concat(prefix.join('|')));
|
|
500
|
-
return [4 /*yield*/, (0, util_1.promptForAnswers)(" Keep or delete (k/d)? ", ['k', 'K', 'd', 'D'])];
|
|
501
|
-
case 5:
|
|
502
568
|
choice = _c.sent();
|
|
569
|
+
_c.label = 5;
|
|
570
|
+
case 5:
|
|
571
|
+
if (!/^(|\d+(,\d+)*)$/.test(choice)) return [3 /*break*/, 3];
|
|
503
572
|
_c.label = 6;
|
|
504
573
|
case 6:
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
else if (source === '11') {
|
|
518
|
-
delete taxon.gbifTaxonID;
|
|
574
|
+
console.error(" Applying selection...");
|
|
575
|
+
if (choice === '') {
|
|
576
|
+
return [2 /*return*/, {}];
|
|
577
|
+
}
|
|
578
|
+
matches = {};
|
|
579
|
+
for (_a = 0, _b = choice.split(','); _a < _b.length; _a++) {
|
|
580
|
+
i = _b[_a];
|
|
581
|
+
prefix = prefixes[parseInt(i) - 1];
|
|
582
|
+
taxa = groupedNameMatches[source][prefix];
|
|
583
|
+
for (id in taxa) {
|
|
584
|
+
if (id in matches) {
|
|
585
|
+
continue;
|
|
519
586
|
}
|
|
520
|
-
|
|
587
|
+
matches[id] = taxa[id];
|
|
521
588
|
}
|
|
522
589
|
}
|
|
523
|
-
return [
|
|
524
|
-
case 7:
|
|
525
|
-
i++;
|
|
526
|
-
return [3 /*break*/, 2];
|
|
527
|
-
case 8:
|
|
528
|
-
_i++;
|
|
529
|
-
return [3 /*break*/, 1];
|
|
530
|
-
case 9: return [2 /*return*/];
|
|
590
|
+
return [2 /*return*/, matches];
|
|
531
591
|
}
|
|
532
592
|
});
|
|
533
593
|
});
|
|
@@ -578,6 +638,9 @@ function main() {
|
|
|
578
638
|
update: {
|
|
579
639
|
type: 'boolean',
|
|
580
640
|
short: 'u'
|
|
641
|
+
},
|
|
642
|
+
'update-mappings': {
|
|
643
|
+
type: 'boolean'
|
|
581
644
|
}
|
|
582
645
|
},
|
|
583
646
|
allowPositionals: true
|
|
@@ -586,7 +649,16 @@ function main() {
|
|
|
586
649
|
process.on('exit', function () {
|
|
587
650
|
process.stdout.write('\n');
|
|
588
651
|
});
|
|
589
|
-
var task
|
|
652
|
+
var task;
|
|
653
|
+
if (args.values.update) {
|
|
654
|
+
task = processor.runUpdate();
|
|
655
|
+
}
|
|
656
|
+
else if (args.values['update-mappings']) {
|
|
657
|
+
task = processor.runMappingsUpdate();
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
task = processor.run();
|
|
661
|
+
}
|
|
590
662
|
task.catch(function (error) {
|
|
591
663
|
console.error(error);
|
|
592
664
|
process.exit(1);
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.csv = exports.resources = exports.catalog = void 0;
|
|
3
|
+
exports.csv = exports.taxonNames = exports.resources = exports.catalog = void 0;
|
|
4
4
|
exports.catalog = require("./catalog/index");
|
|
5
5
|
exports.resources = require("./resources/index");
|
|
6
|
+
exports.taxonNames = require("./taxon-names/index");
|
|
6
7
|
exports.csv = require("./csv");
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.amendResource = exports.groupNameMatches = void 0;
|
|
4
|
+
var MINIMUM_PREFIX_LENGTH = 3;
|
|
5
|
+
var VALID_COMMON_PREFIXES = new Set([
|
|
6
|
+
'Plantae|Tracheophyta',
|
|
7
|
+
'Fungi',
|
|
8
|
+
'Fungi|Ascomycota',
|
|
9
|
+
'Fungi|Basidiomycota',
|
|
10
|
+
'Fungi|Zygomycota'
|
|
11
|
+
]);
|
|
12
|
+
function getCommonPrefix(a, b) {
|
|
13
|
+
for (var i = 0; i < Math.max(a.length, b.length); i++) {
|
|
14
|
+
if (a[i] !== b[i]) {
|
|
15
|
+
return a.slice(0, i);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return a.slice();
|
|
19
|
+
}
|
|
20
|
+
function isValidPrefix(a, b) {
|
|
21
|
+
var prefix = getCommonPrefix(a, b);
|
|
22
|
+
return VALID_COMMON_PREFIXES.has(prefix.join('|')) || prefix.length >= MINIMUM_PREFIX_LENGTH;
|
|
23
|
+
}
|
|
24
|
+
function groupNameMatches(results) {
|
|
25
|
+
var prefixes = {};
|
|
26
|
+
for (var scientificNameID in results) {
|
|
27
|
+
var _loop_1 = function (result) {
|
|
28
|
+
if (!prefixes[result.source]) {
|
|
29
|
+
prefixes[result.source] = [];
|
|
30
|
+
}
|
|
31
|
+
var prefix = prefixes[result.source].find(function (prefix) { return isValidPrefix(prefix[0], result.classificationPath); });
|
|
32
|
+
if (!prefix) {
|
|
33
|
+
prefix = [result.classificationPath, {}];
|
|
34
|
+
prefixes[result.source].push(prefix);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
prefix[0] = getCommonPrefix(prefix[0], result.classificationPath);
|
|
38
|
+
}
|
|
39
|
+
if (scientificNameID in prefix[1]) {
|
|
40
|
+
return "continue";
|
|
41
|
+
}
|
|
42
|
+
prefix[1][scientificNameID] = result;
|
|
43
|
+
};
|
|
44
|
+
for (var _i = 0, _a = results[scientificNameID]; _i < _a.length; _i++) {
|
|
45
|
+
var result = _a[_i];
|
|
46
|
+
_loop_1(result);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
var groupedNameMatches = {};
|
|
50
|
+
for (var source in prefixes) {
|
|
51
|
+
groupedNameMatches[source] = prefixes[source]
|
|
52
|
+
.sort(function (a, b) { return Object.keys(b[1]).length - Object.keys(a[1]).length; })
|
|
53
|
+
.reduce(function (map, _a) {
|
|
54
|
+
var prefix = _a[0], taxa = _a[1];
|
|
55
|
+
map[prefix.join('|')] = taxa;
|
|
56
|
+
return map;
|
|
57
|
+
}, {});
|
|
58
|
+
}
|
|
59
|
+
return groupedNameMatches;
|
|
60
|
+
}
|
|
61
|
+
exports.groupNameMatches = groupNameMatches;
|
|
62
|
+
function amendResource(resource, source, matches) {
|
|
63
|
+
for (var id in matches) {
|
|
64
|
+
var match = matches[id];
|
|
65
|
+
if (source === '1') {
|
|
66
|
+
resource.taxa[id].colTaxonID = match.id;
|
|
67
|
+
if (match.currentId) {
|
|
68
|
+
resource.taxa[id].colAcceptedTaxonID = match.currentId;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else if (source === '11') {
|
|
72
|
+
resource.taxa[id].gbifTaxonID = match.id;
|
|
73
|
+
if (match.currentId) {
|
|
74
|
+
resource.taxa[id].gbifAcceptedTaxonID = match.currentId;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
exports.amendResource = amendResource;
|
package/package.json
CHANGED
|
@@ -21,6 +21,14 @@ function sortObject (object: Record<string, any>): Record<string, any> {
|
|
|
21
21
|
}
|
|
22
22
|
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
23
23
|
|
|
24
|
+
function addTaxon (gbifIndex: Record<string, TaxonId[]>, gbifId: string, taxon: string[]) {
|
|
25
|
+
if (!(gbifId in gbifIndex)) {
|
|
26
|
+
gbifIndex[gbifId] = []
|
|
27
|
+
}
|
|
28
|
+
gbifIndex[gbifId].push(taxon[0])
|
|
29
|
+
gbifIndex[gbifId].sort(numericSort)
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
async function main (args: string[]): Promise<void> {
|
|
25
33
|
const REPO_ROOT = path.resolve(args[0])
|
|
26
34
|
|
|
@@ -50,11 +58,10 @@ async function main (args: string[]): Promise<void> {
|
|
|
50
58
|
for (const taxon of dwc) {
|
|
51
59
|
const gbifId = taxon[25]
|
|
52
60
|
if (gbifId) {
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
addTaxon(gbifIndex, gbifId, taxon)
|
|
62
|
+
if (taxon[27] !== taxon[25]) {
|
|
63
|
+
addTaxon(gbifIndex, taxon[27], taxon)
|
|
55
64
|
}
|
|
56
|
-
gbifIndex[gbifId].push(taxon[0])
|
|
57
|
-
gbifIndex[gbifId].sort(numericSort)
|
|
58
65
|
}
|
|
59
66
|
amendedResource.taxonCount += 1
|
|
60
67
|
}
|
|
@@ -8,17 +8,6 @@ import * as util from 'util'
|
|
|
8
8
|
import { csv } from '../index'
|
|
9
9
|
import { prompt, promptForAnswers, numericSort, runCommand } from './util'
|
|
10
10
|
|
|
11
|
-
interface AmendedTaxon extends Taxon {
|
|
12
|
-
colTaxonID?: string,
|
|
13
|
-
gbifTaxonID?: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface AmendedResource extends Resource {
|
|
17
|
-
taxa: Record<TaxonId, AmendedTaxon>
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
type Classifications = Record<string, Array<[AmendedTaxon, string]>>
|
|
21
|
-
|
|
22
11
|
const DWC_FIELDS: string[] = [
|
|
23
12
|
'scientificNameID',
|
|
24
13
|
'scientificName',
|
|
@@ -49,7 +38,9 @@ const DWC_FIELDS: string[] = [
|
|
|
49
38
|
'higherClassification',
|
|
50
39
|
|
|
51
40
|
'colTaxonID',
|
|
52
|
-
'gbifTaxonID'
|
|
41
|
+
'gbifTaxonID',
|
|
42
|
+
'colAcceptedTaxonID',
|
|
43
|
+
'gbifAcceptedTaxonID'
|
|
53
44
|
]
|
|
54
45
|
|
|
55
46
|
const DISPLAY_FIELDS: string[] = [
|
|
@@ -74,17 +65,9 @@ const GBIF_RANKS: Rank[] = [
|
|
|
74
65
|
'variety'
|
|
75
66
|
]
|
|
76
67
|
|
|
77
|
-
const VALID_COMMON_PREFIXES = [
|
|
78
|
-
'Plantae|Tracheophyta',
|
|
79
|
-
'Fungi',
|
|
80
|
-
'Fungi|Ascomycota',
|
|
81
|
-
'Fungi|Basidiomycota',
|
|
82
|
-
'Fungi|Zygomycota'
|
|
83
|
-
]
|
|
84
|
-
|
|
85
68
|
function runGnverifier (names: string): Promise<string> {
|
|
86
69
|
return new Promise((resolve, reject) => {
|
|
87
|
-
const proc = spawn('gnverifier', ['-s', '1,11', '-M'])
|
|
70
|
+
const proc = spawn('gnverifier', ['-s', '1,11', '-f', 'compact', '-M'])
|
|
88
71
|
let stdout = ''
|
|
89
72
|
proc.stdout.on('data', data => { stdout += data })
|
|
90
73
|
proc.stderr.pipe(process.stdout)
|
|
@@ -153,6 +136,18 @@ class ResourceProcessor {
|
|
|
153
136
|
}
|
|
154
137
|
}
|
|
155
138
|
|
|
139
|
+
async runMappingsUpdate (): Promise<void> {
|
|
140
|
+
const input = await fs.readdir(this.DIR_TXT)
|
|
141
|
+
|
|
142
|
+
const ids = input
|
|
143
|
+
.map(file => path.basename(file, '.txt'))
|
|
144
|
+
.sort((a, b) => parseInt(a.slice(1)) - parseInt(b.slice(1)))
|
|
145
|
+
|
|
146
|
+
for (const id of ids) {
|
|
147
|
+
await this.processWork(id, true)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
156
151
|
async processWork (id: WorkId, update?: boolean): Promise<void> {
|
|
157
152
|
const resources = await this.processResources(id, update)
|
|
158
153
|
|
|
@@ -174,11 +169,7 @@ class ResourceProcessor {
|
|
|
174
169
|
|
|
175
170
|
const amendedResources = []
|
|
176
171
|
for (const resource of resources) {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
for (const source in classifications) {
|
|
180
|
-
await this.checkPrefix(resource, classifications, source)
|
|
181
|
-
}
|
|
172
|
+
const results = await this.processResourceDwc(resource)
|
|
182
173
|
|
|
183
174
|
const skip = await this.shouldBeSkipped(resource.id)
|
|
184
175
|
|
|
@@ -256,100 +247,153 @@ class ResourceProcessor {
|
|
|
256
247
|
}
|
|
257
248
|
}
|
|
258
249
|
|
|
259
|
-
async processResourceDwc (resource: Resource): Promise<
|
|
250
|
+
async processResourceDwc (resource: Resource): Promise<AmendedResource> {
|
|
260
251
|
console.log(`${resource.workId}: matching ${resource.id}`)
|
|
261
|
-
|
|
262
|
-
const
|
|
252
|
+
|
|
253
|
+
const filteredResults: Record<TaxonId, TaxonMatch[]> = {}
|
|
254
|
+
const taxonNames: Record<string, TaxonId[]> = {}
|
|
255
|
+
const names = new Set()
|
|
263
256
|
for (const id in resource.taxa) {
|
|
264
257
|
const name = resource.taxa[id].scientificName
|
|
265
258
|
|
|
266
|
-
if (!
|
|
267
|
-
|
|
259
|
+
if (!taxonNames[name]) { taxonNames[name] = [] }
|
|
260
|
+
taxonNames[name].push(id)
|
|
268
261
|
|
|
269
|
-
names.
|
|
262
|
+
names.add(name)
|
|
263
|
+
filteredResults[id] = []
|
|
270
264
|
}
|
|
271
265
|
|
|
272
|
-
const result = await runGnverifier(names.join('\n'))
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (
|
|
285
|
-
|
|
286
|
-
|
|
266
|
+
const result = await runGnverifier(Array.from(names).join('\n'))
|
|
267
|
+
for (const results of result.trim().split('\n')) {
|
|
268
|
+
const { name, results: matches } = JSON.parse(results)
|
|
269
|
+
|
|
270
|
+
if (!matches) {
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for (const match of matches) {
|
|
275
|
+
const source = match.dataSourceId
|
|
276
|
+
const currentRank = match.classificationRanks.split('|').pop()
|
|
277
|
+
|
|
278
|
+
if (match.scoreDetails.cardinalityScore === 0) {
|
|
279
|
+
// Rank mismatch
|
|
280
|
+
continue
|
|
281
|
+
} else if (source === 11 && currentRank === 'species' && match.classificationPath.endsWith(' spec')) {
|
|
282
|
+
// GBIF species like "Nomada spec"
|
|
283
|
+
continue
|
|
287
284
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
285
|
+
|
|
286
|
+
for (const loirId of taxonNames[name]) {
|
|
287
|
+
const taxon = resource.taxa[loirId]
|
|
288
|
+
|
|
289
|
+
if (source === 11 && !GBIF_RANKS.includes(taxon.taxonRank)) {
|
|
290
|
+
// Exclude GBIF matches for ranks that are not in GBIF
|
|
291
|
+
continue
|
|
292
|
+
} else if (source === 11 && !match.isSynonym && currentRank !== taxon.taxonRank) {
|
|
293
|
+
// Exclude matches with rank mismatches (only possible
|
|
294
|
+
// for non-synonyms).
|
|
295
|
+
continue
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!filteredResults[loirId]) {
|
|
299
|
+
filteredResults[loirId] = []
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
filteredResults[loirId].push({
|
|
303
|
+
source,
|
|
304
|
+
id: match.recordId,
|
|
305
|
+
currentId: match.currentRecordId,
|
|
306
|
+
classificationPath: match.classificationPath.split('|')
|
|
307
|
+
})
|
|
291
308
|
}
|
|
292
309
|
}
|
|
293
310
|
}
|
|
294
311
|
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
312
|
+
const { taxonNames: { amendResource, groupNameMatches } } = await import('../index')
|
|
313
|
+
const groupedNameMatches = groupNameMatches(filteredResults)
|
|
314
|
+
|
|
315
|
+
const amendedResource: AmendedResource = { ...resource, taxa: { ...resource.taxa } }
|
|
316
|
+
for (const source in groupedNameMatches) {
|
|
317
|
+
const matches = await this.selectPrefixes(resource, groupedNameMatches, source)
|
|
318
|
+
amendResource(amendedResource, source, matches)
|
|
301
319
|
}
|
|
302
320
|
|
|
303
|
-
return
|
|
321
|
+
return amendedResource
|
|
304
322
|
}
|
|
305
323
|
|
|
306
|
-
async
|
|
307
|
-
const
|
|
308
|
-
if (
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
for (let i = 0; i < parts.length; i++) {
|
|
314
|
-
if (parts[i] !== prefix[i] && i < 3) {
|
|
315
|
-
let choice
|
|
316
|
-
|
|
317
|
-
if (source === '1' || taxon.taxonomicStatus !== 'accepted') {
|
|
318
|
-
choice = 'd'
|
|
319
|
-
} else if (VALID_COMMON_PREFIXES.includes(prefix.slice(0, i).join('|'))) {
|
|
320
|
-
choice = 'k'
|
|
321
|
-
} else {
|
|
322
|
-
console.log(`${resource.workId}: source ${source} results in short prefix "${prefix.slice(0, i).join('|')}" (${i} taxa)`)
|
|
323
|
-
console.log(` taxon: ${taxon.scientificNameID} "${taxon.scientificName}"`)
|
|
324
|
-
console.log(` class: ${parts.join('|')}`)
|
|
325
|
-
console.log(` prefx: ${prefix.join('|')}`)
|
|
326
|
-
|
|
327
|
-
choice = await promptForAnswers(` Keep or delete (k/d)? `, ['k', 'K', 'd', 'D'])
|
|
328
|
-
}
|
|
324
|
+
async selectPrefixes (resource: Resource, groupedNameMatches: GroupedNameMatches, source: string): Promise<Record<TaxonId, TaxonMatch>> {
|
|
325
|
+
const prefixes = Object.keys(groupedNameMatches[source])
|
|
326
|
+
if (prefixes.length === 0) {
|
|
327
|
+
return {}
|
|
328
|
+
} else if (prefixes.length === 1) {
|
|
329
|
+
return groupedNameMatches[source][prefixes[0]]
|
|
330
|
+
}
|
|
329
331
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
332
|
+
// Count total mapped taxa
|
|
333
|
+
const mappedTaxa: Record<TaxonId, boolean> = {}
|
|
334
|
+
for (const prefix of prefixes) {
|
|
335
|
+
for (const taxon in groupedNameMatches[source][prefix]) {
|
|
336
|
+
mappedTaxa[taxon] = true
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const missedTaxonCount = Object.keys(mappedTaxa).length - Object.keys(groupedNameMatches[source][prefixes[0]]).length
|
|
336
340
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
341
|
+
if (missedTaxonCount === 0) {
|
|
342
|
+
// Multiple prefixes but the first one maps all taxa (not counting that are unmapped in all prefixes)
|
|
343
|
+
return groupedNameMatches[source][prefixes[0]]
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
console.error(`${resource.workId}: source ${source} results in multiple prefixes`)
|
|
347
|
+
|
|
348
|
+
let choice
|
|
349
|
+
if (missedTaxonCount <= 5) {
|
|
350
|
+
console.error(` Most common prefix misses ${missedTaxonCount} taxa: automatically selecting most common prefix...`)
|
|
351
|
+
choice = '1'
|
|
352
|
+
} else if (source === '1') {
|
|
353
|
+
console.error(` Catalogue of Life: automatically selecting most common prefix...`)
|
|
354
|
+
choice = '1'
|
|
355
|
+
} else {
|
|
356
|
+
for (let i = 0; i < prefixes.length; i++) {
|
|
357
|
+
const prefix = prefixes[i]
|
|
358
|
+
const taxa = groupedNameMatches[source][prefix]
|
|
359
|
+
const taxonIds = Object.keys(taxa)
|
|
360
|
+
|
|
361
|
+
console.error(` [${i + 1}] ${prefix} (${taxonIds.length} taxa)`)
|
|
362
|
+
for (let j = 0; j < Math.min(9, taxonIds.length); j++) {
|
|
363
|
+
const taxonId = taxonIds[j]
|
|
364
|
+
const taxon = resource.taxa[taxonId]
|
|
365
|
+
const match = taxa[taxonId]
|
|
366
|
+
console.error(` taxon: ${taxonId} "${taxon.scientificName}" - ${match.classificationPath.join('|')}`)
|
|
367
|
+
}
|
|
368
|
+
if (taxonIds.length > 9) {
|
|
369
|
+
console.error(` ...`)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
do {
|
|
374
|
+
choice = await prompt(` Select prefixes (1-${prefixes.length})? `)
|
|
375
|
+
} while (!/^(|\d+(,\d+)*)$/.test(choice))
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
console.error(` Applying selection...`)
|
|
379
|
+
|
|
380
|
+
if (choice === '') {
|
|
381
|
+
return {}
|
|
382
|
+
}
|
|
348
383
|
|
|
349
|
-
|
|
384
|
+
const matches: Record<TaxonId, TaxonMatch> = {}
|
|
385
|
+
for (const i of choice.split(',')) {
|
|
386
|
+
const prefix = prefixes[parseInt(i) - 1]
|
|
387
|
+
const taxa = groupedNameMatches[source][prefix]
|
|
388
|
+
for (const id in taxa) {
|
|
389
|
+
if (id in matches) {
|
|
390
|
+
continue
|
|
350
391
|
}
|
|
392
|
+
matches[id] = taxa[id]
|
|
351
393
|
}
|
|
352
394
|
}
|
|
395
|
+
|
|
396
|
+
return matches
|
|
353
397
|
}
|
|
354
398
|
|
|
355
399
|
checkResults (resource: AmendedResource): boolean {
|
|
@@ -388,6 +432,9 @@ function main (): void {
|
|
|
388
432
|
update: {
|
|
389
433
|
type: 'boolean',
|
|
390
434
|
short: 'u'
|
|
435
|
+
},
|
|
436
|
+
'update-mappings': {
|
|
437
|
+
type: 'boolean'
|
|
391
438
|
}
|
|
392
439
|
},
|
|
393
440
|
allowPositionals: true
|
|
@@ -398,7 +445,15 @@ function main (): void {
|
|
|
398
445
|
process.stdout.write('\n')
|
|
399
446
|
})
|
|
400
447
|
|
|
401
|
-
|
|
448
|
+
let task
|
|
449
|
+
if (args.values.update) {
|
|
450
|
+
task = processor.runUpdate()
|
|
451
|
+
} else if (args.values['update-mappings']) {
|
|
452
|
+
task = processor.runMappingsUpdate()
|
|
453
|
+
} else {
|
|
454
|
+
task = processor.run()
|
|
455
|
+
}
|
|
456
|
+
|
|
402
457
|
task.catch(error => {
|
|
403
458
|
console.error(error)
|
|
404
459
|
process.exit(1)
|
package/src/index.ts
CHANGED
package/src/module.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
type Schema = Record<string, FieldSpecification>
|
|
2
2
|
type FieldSpecification = {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
required: boolean,
|
|
4
|
+
multiple: boolean | FieldSpecificationCallbackMultiple
|
|
5
|
+
format?: string[] | RegExp | FieldSpecificationCallbackFormat
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
type Value = string[] | string
|
|
@@ -11,12 +11,12 @@ type FieldSpecificationCallbackMultiple = (entry: Record<string, Value>) => bool
|
|
|
11
11
|
type FieldSpecificationCallbackFormat = (value: SingleValue) => boolean;
|
|
12
12
|
|
|
13
13
|
interface FieldError {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
field: string,
|
|
15
|
+
error: string
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
interface WorkError extends FieldError {
|
|
19
|
-
|
|
19
|
+
entity: WorkId
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
type Rank = string
|
|
@@ -28,59 +28,59 @@ type ResourceId = string
|
|
|
28
28
|
type WorkId = string
|
|
29
29
|
|
|
30
30
|
interface WorkingTaxon {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
31
|
+
scientificNameID?: TaxonId,
|
|
32
|
+
scientificName?: string,
|
|
33
|
+
scientificNameAuthorship?: string,
|
|
34
|
+
genericName?: string,
|
|
35
|
+
infragenericEpithet?: string,
|
|
36
|
+
specificEpithet?: string,
|
|
37
|
+
intraspecificEpithet?: string,
|
|
38
|
+
|
|
39
|
+
taxonRank?: Rank,
|
|
40
|
+
taxonRemarks?: string,
|
|
41
|
+
collectionCode?: ResourceId,
|
|
42
|
+
|
|
43
|
+
taxonomicStatus?: TaxonStatus,
|
|
44
|
+
acceptedNameUsageID?: TaxonId,
|
|
45
|
+
acceptedNameUsage?: string,
|
|
46
|
+
|
|
47
|
+
parentNameUsageID?: TaxonId,
|
|
48
|
+
parentNameUsage?: string,
|
|
49
|
+
kingdom?: string,
|
|
50
|
+
phylum?: string,
|
|
51
|
+
class?: string,
|
|
52
|
+
order?: string,
|
|
53
|
+
family?: string,
|
|
54
|
+
subfamily?: string,
|
|
55
|
+
genus?: string,
|
|
56
|
+
subgenus?: string,
|
|
57
|
+
higherClassification?: string,
|
|
58
|
+
|
|
59
|
+
// Non-standard
|
|
60
|
+
scientificNameOnly?: string,
|
|
61
|
+
incorrect?: WorkingTaxon
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
interface Taxon extends WorkingTaxon {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
scientificNameID: TaxonId,
|
|
66
|
+
scientificName: string,
|
|
67
|
+
taxonRank: Rank,
|
|
68
|
+
collectionCode: ResourceId,
|
|
69
|
+
taxonomicStatus: string
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
interface ResourceMetadata {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
levels: Rank[],
|
|
74
|
+
scope: string[],
|
|
75
|
+
catalog?: object
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
interface Resource {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
id: string,
|
|
80
|
+
file: string,
|
|
81
|
+
workId: string,
|
|
82
|
+
metadata: ResourceMetadata,
|
|
83
|
+
taxa: Record<TaxonId, Taxon>
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
interface ResourceHistory {
|
|
@@ -103,3 +103,23 @@ declare enum ResourceDiffType {
|
|
|
103
103
|
Modified = '~',
|
|
104
104
|
Unchanged = '='
|
|
105
105
|
}
|
|
106
|
+
|
|
107
|
+
interface AmendedTaxon extends Taxon {
|
|
108
|
+
colTaxonID?: string,
|
|
109
|
+
colAcceptedTaxonID?: string,
|
|
110
|
+
gbifTaxonID?: string,
|
|
111
|
+
gbifAcceptedTaxonID?: string
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface AmendedResource extends Resource {
|
|
115
|
+
taxa: Record<TaxonId, AmendedTaxon>
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface TaxonMatch {
|
|
119
|
+
source: number,
|
|
120
|
+
id: string,
|
|
121
|
+
currentId?: string,
|
|
122
|
+
classificationPath: string[]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
type GroupedNameMatches = Record<string, Record<string, Record<TaxonId, TaxonMatch>>>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const MINIMUM_PREFIX_LENGTH = 3
|
|
2
|
+
const VALID_COMMON_PREFIXES = new Set([
|
|
3
|
+
'Plantae|Tracheophyta',
|
|
4
|
+
'Fungi',
|
|
5
|
+
'Fungi|Ascomycota',
|
|
6
|
+
'Fungi|Basidiomycota',
|
|
7
|
+
'Fungi|Zygomycota'
|
|
8
|
+
])
|
|
9
|
+
|
|
10
|
+
function getCommonPrefix (a: string[], b: string[]): string[] {
|
|
11
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
12
|
+
if (a[i] !== b[i]) {
|
|
13
|
+
return a.slice(0, i)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return a.slice()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isValidPrefix (a: string[], b: string[]): boolean {
|
|
20
|
+
const prefix = getCommonPrefix(a, b)
|
|
21
|
+
return VALID_COMMON_PREFIXES.has(prefix.join('|')) || prefix.length >= MINIMUM_PREFIX_LENGTH
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function groupNameMatches (results: Record<TaxonId, TaxonMatch[]>): GroupedNameMatches {
|
|
25
|
+
const prefixes: Record<string, [string[], Record<TaxonId, TaxonMatch>][]> = {}
|
|
26
|
+
|
|
27
|
+
for (const scientificNameID in results) {
|
|
28
|
+
for (const result of results[scientificNameID]) {
|
|
29
|
+
if (!prefixes[result.source]) {
|
|
30
|
+
prefixes[result.source] = []
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let prefix = prefixes[result.source].find(prefix => isValidPrefix(prefix[0], result.classificationPath))
|
|
34
|
+
|
|
35
|
+
if (!prefix) {
|
|
36
|
+
prefix = [result.classificationPath, {}]
|
|
37
|
+
prefixes[result.source].push(prefix)
|
|
38
|
+
} else {
|
|
39
|
+
prefix[0] = getCommonPrefix(prefix[0], result.classificationPath)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (scientificNameID in prefix[1]) {
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
prefix[1][scientificNameID] = result
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const groupedNameMatches: GroupedNameMatches = {}
|
|
51
|
+
for (const source in prefixes) {
|
|
52
|
+
groupedNameMatches[source] = prefixes[source]
|
|
53
|
+
.sort((a, b) => Object.keys(b[1]).length - Object.keys(a[1]).length)
|
|
54
|
+
.reduce((map: Record<string, Record<TaxonId, TaxonMatch>>, [prefix, taxa]) => {
|
|
55
|
+
map[prefix.join('|')] = taxa
|
|
56
|
+
return map
|
|
57
|
+
}, {})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return groupedNameMatches
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function amendResource (resource: AmendedResource, source: string, matches: Record<TaxonId, TaxonMatch>) {
|
|
64
|
+
for (const id in matches) {
|
|
65
|
+
const match = matches[id]
|
|
66
|
+
|
|
67
|
+
if (source === '1') {
|
|
68
|
+
resource.taxa[id].colTaxonID = match.id
|
|
69
|
+
if (match.currentId) {
|
|
70
|
+
resource.taxa[id].colAcceptedTaxonID = match.currentId
|
|
71
|
+
}
|
|
72
|
+
} else if (source === '11') {
|
|
73
|
+
resource.taxa[id].gbifTaxonID = match.id
|
|
74
|
+
if (match.currentId) {
|
|
75
|
+
resource.taxa[id].gbifAcceptedTaxonID = match.currentId
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|