@larsgw/formica 0.9.2 → 0.10.1
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 +34 -13
- package/lib/bin/validate-resources-text.js +3 -1
- package/lib/catalog/tables/taxon.js +4 -0
- package/lib/catalog/value.d.ts +1 -0
- package/lib/catalog/value.js +1 -0
- 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,762 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { existsSync as doesFileExist, promises as fs } from 'fs'
|
|
4
|
-
import * as path from 'path'
|
|
5
|
-
import * as util from 'util'
|
|
6
|
-
|
|
7
|
-
import type { JsonLdDocument, NodeObject } from 'jsonld'
|
|
8
|
-
import * as jsonld from 'jsonld'
|
|
9
|
-
import * as N3 from 'n3'
|
|
10
|
-
|
|
11
|
-
import { catalog } from '../index'
|
|
12
|
-
import { WorkResource } from '../resources/resource'
|
|
13
|
-
import { parseCsv } from '../csv'
|
|
14
|
-
|
|
15
|
-
const SHEETS = ['catalog', 'authors', 'places', 'publishers', 'taxa'] as const
|
|
16
|
-
|
|
17
|
-
interface Resource {
|
|
18
|
-
metadata: ResourceMetadata,
|
|
19
|
-
taxa: AmendedTaxon[],
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface Catalog {
|
|
23
|
-
catalog: catalog.Entities,
|
|
24
|
-
authors: catalog.Entities,
|
|
25
|
-
places: catalog.Entities,
|
|
26
|
-
publishers: catalog.Entities,
|
|
27
|
-
taxa: catalog.Entities,
|
|
28
|
-
|
|
29
|
-
resources: Record<ResourceId, Resource>,
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const PREFIX = 'https://purl.org/identification-resources/'
|
|
33
|
-
const HANDLE_PREFIX = 'https://hdl.handle.net/'
|
|
34
|
-
const SCOPES: Record<string, [string, string]> = {
|
|
35
|
-
// animal life stage
|
|
36
|
-
'adults': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/adult'],
|
|
37
|
-
'pupae': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/pupa'],
|
|
38
|
-
'juveniles': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/juvenile'],
|
|
39
|
-
'subimagos': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/juvenile'],
|
|
40
|
-
'larvae': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
|
|
41
|
-
'larvae (instar V)': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
|
|
42
|
-
'larvae (instar IV)': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
|
|
43
|
-
'larvae (instar III)': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
|
|
44
|
-
'larvae (instar I)': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
|
|
45
|
-
'nymphs': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
|
|
46
|
-
'nypmhs': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
|
|
47
|
-
'nymphs (instar V)': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
|
|
48
|
-
'nymphs (instar IV)': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
|
|
49
|
-
'eggs': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/embryo'],
|
|
50
|
-
|
|
51
|
-
// plant life stage
|
|
52
|
-
'flowering plants': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/adult'],
|
|
53
|
-
'fruiting plants': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/adult'],
|
|
54
|
-
'without sporangia': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/juvenile'],
|
|
55
|
-
'with sporangia': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/adult'],
|
|
56
|
-
'teleomorphs': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/adult'],
|
|
57
|
-
|
|
58
|
-
// sex
|
|
59
|
-
'females': ['dwciri:sex', 'http://rs.gbif.org/vocabulary/gbif/sex/female'],
|
|
60
|
-
'males': ['dwciri:sex', 'http://rs.gbif.org/vocabulary/gbif/sex/male'],
|
|
61
|
-
'male': ['dwciri:sex', 'http://rs.gbif.org/vocabulary/gbif/sex/male'],
|
|
62
|
-
|
|
63
|
-
// caste
|
|
64
|
-
'queens': ['dwc:caste', 'queen'],
|
|
65
|
-
'workers': ['dwc:caste', 'worker'],
|
|
66
|
-
'soldiers': ['dwc:caste', 'soldier'],
|
|
67
|
-
'alatae': ['dwc:caste', 'alate'],
|
|
68
|
-
'apterae': ['dwc:caste', 'aptera'],
|
|
69
|
-
'viviparae': ['dwc:caste', 'vivipara'],
|
|
70
|
-
|
|
71
|
-
// evidence
|
|
72
|
-
'nests': ['ac:subjectPartLiteral', 'nest'],
|
|
73
|
-
'galls': ['ac:subjectPartLiteral', 'gall'],
|
|
74
|
-
'puparia': ['ac:subjectPartLiteral', 'puparium'],
|
|
75
|
-
'eggcases': ['ac:subjectPart', 'http://rs.tdwg.org/acpart/values/p0031'],
|
|
76
|
-
'bones': ['ac:subjectPartLiteral', 'skeleton'],
|
|
77
|
-
'bones (skulls)': ['ac:subjectPart', 'http://rs.tdwg.org/acpart/values/p0027'],
|
|
78
|
-
'bones (upper jaws)': ['ac:subjectPart', 'http://rs.tdwg.org/acpart/values/p0028'],
|
|
79
|
-
'bones (lower jaws)': ['ac:subjectPart', 'http://rs.tdwg.org/acpart/values/p0029'],
|
|
80
|
-
}
|
|
81
|
-
const PREFIXES = {
|
|
82
|
-
'ac': 'http://rs.tdwg.org/ac/terms/',
|
|
83
|
-
'bibo': 'http://purl.org/ontology/bibo/',
|
|
84
|
-
'dcterms': 'http://purl.org/dc/terms/',
|
|
85
|
-
'dwc': 'http://rs.tdwg.org/dwc/terms/',
|
|
86
|
-
'dwciri': 'http://rs.tdwg.org/dwc/iri/',
|
|
87
|
-
'foaf': 'http://xmlns.com/foaf/0.1/',
|
|
88
|
-
'owl': 'http://www.w3.org/2002/07/owl#',
|
|
89
|
-
'rdfs': 'http://www.w3.org/2000/01/rdf-schema#',
|
|
90
|
-
'schema': 'https://schema.org/',
|
|
91
|
-
'xsd': 'http://www.w3.org/2001/XMLSchema#',
|
|
92
|
-
}
|
|
93
|
-
const DWC_FIELDS: Record<string, string> = {
|
|
94
|
-
scientificName: 'dwc:scientificName',
|
|
95
|
-
scientificNameAuthorship: 'dwc:scientificNameAuthorship',
|
|
96
|
-
genericName: 'dwc:genericName',
|
|
97
|
-
infragenericEpithet: 'dwc:infragenericEpithet',
|
|
98
|
-
specificEpithet: 'dwc:specificEpithet',
|
|
99
|
-
infraspecificEpithet: 'dwc:infraspecificEpithet',
|
|
100
|
-
taxonRank: 'dwc:taxonRank',
|
|
101
|
-
taxonRemarks: 'dwc:taxonRemarks',
|
|
102
|
-
taxonomicStatus: 'dwc:taxonomicStatus',
|
|
103
|
-
verbatimIdentification: 'dwc:verbatimIdentification',
|
|
104
|
-
}
|
|
105
|
-
const GBIF_RANKS = ['kingdom', 'phylum', 'class', 'order', 'family', 'genus', 'species', 'subspecies']
|
|
106
|
-
const GBIF_VOCAB_RANKS = ['domain', 'kingdom', 'subkingdom', 'superphylum', 'phylum', 'subphylum', 'superclass', 'class', 'subclass', 'supercohort', 'cohort', 'subcohort', 'superorder', 'order', 'suborder', 'infraorder', 'superfamily', 'family', 'subfamily', 'tribe', 'subtribe', 'genus', 'subgenus', 'section', 'subsection', 'series', 'subseries', 'speciesAggregate', 'species', 'subspecificAggregate', 'subspecies', 'variety', 'subvariety', 'form', 'subform', 'cultivarGroup', 'cultivar', 'strain']
|
|
107
|
-
const STATUSES: Record<string, string> = {
|
|
108
|
-
'accepted': 'http://rs.gbif.org/vocabulary/gbif/taxonomicStatus/accepted',
|
|
109
|
-
'misapplied': 'http://rs.gbif.org/vocabulary/gbif/taxonomicStatus/misapplied',
|
|
110
|
-
'proparte synonym': 'http://rs.gbif.org/vocabulary/gbif/taxonomicStatus/proParteSynonym',
|
|
111
|
-
'synonym': 'http://rs.gbif.org/vocabulary/gbif/taxonomicStatus/synonym',
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function getCoveringTaxon (taxa: catalog.Entity[]): string|null {
|
|
115
|
-
if (taxa.length === 1 && !taxa[0].has('gbif')) {
|
|
116
|
-
return `${PREFIX}taxon/${taxa[0].get('id')}`
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function getAncestors (taxon: catalog.Entity): string[] {
|
|
120
|
-
const [...parents] = (taxon.get('parent_taxa') ?? []) as string[]
|
|
121
|
-
if (taxon.has('gbif')) {
|
|
122
|
-
parents.push(taxon.get('gbif') as string)
|
|
123
|
-
}
|
|
124
|
-
return parents
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const ancestors = getAncestors(taxa[0])
|
|
128
|
-
for (const taxon of taxa.slice(1)) {
|
|
129
|
-
const otherAncestors = getAncestors(taxon)
|
|
130
|
-
|
|
131
|
-
for (let i = 0; i < ancestors.length; i++) {
|
|
132
|
-
if (ancestors[i] !== otherAncestors[i]) {
|
|
133
|
-
ancestors.splice(i)
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return ancestors.length ? makeGbifUri(ancestors[ancestors.length - 1])['@id'] as string : null
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function mapEntities (names: string[], entities: catalog.Entity[]): catalog.Entity[] {
|
|
142
|
-
const result = []
|
|
143
|
-
|
|
144
|
-
for (const name of names) {
|
|
145
|
-
const entity = entities.find(entity => {
|
|
146
|
-
const values = entity.get('name')
|
|
147
|
-
return Array.isArray(values) ? values.includes(name) : values === name
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
if (!entity) {
|
|
151
|
-
console.error('Unmapped entity:', name)
|
|
152
|
-
continue
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
result.push(entity as catalog.Entity)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return result
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function makeWikidataUri (qid: string): NodeObject {
|
|
162
|
-
return { '@id': `http://www.wikidata.org/entity/${qid}` }
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function makeGbifUri (id: string): NodeObject {
|
|
166
|
-
return { '@id': `https://gbif.org/species/${id}` }
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function makeWorkUri (id: string): NodeObject {
|
|
170
|
-
return { '@id': `${PREFIX}catalog/${id}` }
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function makeScientificNameUri (id: string): NodeObject {
|
|
174
|
-
const resource = id.split(':').slice(0, -1).join(':')
|
|
175
|
-
return { '@id': `${PREFIX}resource/${resource}#${id}` }
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function makeTaxonRankUri (rank: string): NodeObject|string {
|
|
179
|
-
if (GBIF_VOCAB_RANKS.includes(rank)) {
|
|
180
|
-
return { '@id': `http://rs.gbif.org/vocabulary/gbif/rank/${rank}` }
|
|
181
|
-
} else {
|
|
182
|
-
return rank
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function makeTaxonomicStatusUri (status: keyof typeof STATUSES): NodeObject {
|
|
187
|
-
return { '@id': STATUSES[status] }
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function makeLinkedDataForAuthor (author: catalog.Entity): NodeObject {
|
|
191
|
-
const node: NodeObject = {
|
|
192
|
-
'@id': `${PREFIX}author/${author.get('id')}`,
|
|
193
|
-
'@type': 'foaf:Person',
|
|
194
|
-
'foaf:name': author.get('display_name')
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (author.has('qid')) {
|
|
198
|
-
node['owl:sameAs'] = makeWikidataUri(author.get('qid') as string)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return node
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function makeLinkedDataForPlace (place: catalog.Entity): NodeObject {
|
|
205
|
-
const node: NodeObject = {
|
|
206
|
-
'@id': `${PREFIX}place/${place.get('id')}`,
|
|
207
|
-
'@type': 'dcterms:Location',
|
|
208
|
-
'dcterms:title': place.get('display_name'),
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (place.has('qid')) {
|
|
212
|
-
node['owl:sameAs'] = makeWikidataUri(place.get('qid') as string)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return node
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function makeLinkedDataForPublisher (publisher: catalog.Entity): NodeObject {
|
|
219
|
-
const node: NodeObject = {
|
|
220
|
-
'@id': `${PREFIX}publisher/${publisher.get('id')}`,
|
|
221
|
-
'@type': 'foaf:Organization',
|
|
222
|
-
'foaf:name': publisher.get('display_name')
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (publisher.has('qid')) {
|
|
226
|
-
node['owl:sameAs'] = makeWikidataUri(publisher.get('qid') as string)
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return node
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function makeLinkedDataForTaxon (taxon: catalog.Entity): NodeObject {
|
|
233
|
-
const node: NodeObject = {
|
|
234
|
-
'@id': `${PREFIX}taxon/${taxon.get('id')}`,
|
|
235
|
-
'@type': 'dwc:Taxon',
|
|
236
|
-
'dwc:scientificName': taxon.get('name'),
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (taxon.has('rank')) {
|
|
240
|
-
node['dwc:taxonRank'] = makeTaxonRankUri(taxon.get('rank') as string)
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const ids = []
|
|
244
|
-
if (taxon.has('qid')) {
|
|
245
|
-
ids.push(makeWikidataUri(taxon.get('qid') as string))
|
|
246
|
-
}
|
|
247
|
-
if (taxon.has('gbif')) {
|
|
248
|
-
ids.push(makeGbifUri(taxon.get('gbif') as string))
|
|
249
|
-
}
|
|
250
|
-
if (ids.length) {
|
|
251
|
-
node['owl:sameAs'] = ids
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return node
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function makeLinkedDataForTaxa (files: Catalog): NodeObject[] {
|
|
258
|
-
const nodes = []
|
|
259
|
-
const gbifTaxa: Record<string, NodeObject> = {}
|
|
260
|
-
|
|
261
|
-
for (const taxon of files.taxa.entities) {
|
|
262
|
-
const node = makeLinkedDataForTaxon(taxon)
|
|
263
|
-
|
|
264
|
-
const ancestors = taxon.get('ancestors_gbif') ?? []
|
|
265
|
-
if (ancestors.length) {
|
|
266
|
-
node['dwc:parentNameUsageID'] = makeGbifUri(ancestors[ancestors.length - 1])
|
|
267
|
-
} else if (taxon.get('id') !== 'T141') {
|
|
268
|
-
node['dwc:parentNameUsageID'] = { '@id': `${PREFIX}taxon/T141` }
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
for (let i = 0; i < ancestors.length; i++) {
|
|
272
|
-
gbifTaxa[ancestors[i]] = {
|
|
273
|
-
...makeGbifUri(ancestors[i]),
|
|
274
|
-
'dwc:taxonRank': makeTaxonRankUri(GBIF_RANKS[i]),
|
|
275
|
-
'dwc:parentNameUsageID': i ? makeGbifUri(ancestors[i - 1]) : { '@id': `${PREFIX}taxon/T141` }
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (taxon.has('children_gbif')) {
|
|
280
|
-
const children = taxon.get('children_gbif') as string[]
|
|
281
|
-
node['@reverse'] = { 'dwc:parentNameUsageID': children.map(makeGbifUri) as unknown as string }
|
|
282
|
-
|
|
283
|
-
const childRank = GBIF_RANKS[ancestors.length]
|
|
284
|
-
for (const child of children) {
|
|
285
|
-
gbifTaxa[child] = {
|
|
286
|
-
...makeGbifUri(child),
|
|
287
|
-
'dwc:taxonRank': makeTaxonRankUri(childRank),
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
nodes.push(node)
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
nodes.push(...Object.values(gbifTaxa))
|
|
296
|
-
|
|
297
|
-
return nodes
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function makeLinkedDataForScientificName (name: AmendedTaxon): NodeObject {
|
|
301
|
-
const node: NodeObject = {
|
|
302
|
-
...makeScientificNameUri(name.scientificNameID),
|
|
303
|
-
'@type': 'dwc:Taxon',
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
for (const field in DWC_FIELDS) {
|
|
307
|
-
const value = name[field as keyof AmendedTaxon]
|
|
308
|
-
if (value) {
|
|
309
|
-
node[DWC_FIELDS[field]] = value as string
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (GBIF_VOCAB_RANKS.includes(name.taxonRank)) {
|
|
314
|
-
node[DWC_FIELDS.taxonRank] = makeTaxonRankUri(name.taxonRank)
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (name.taxonomicStatus in STATUSES) {
|
|
318
|
-
node[DWC_FIELDS.taxonomicStatus] = makeTaxonomicStatusUri(name.taxonomicStatus)
|
|
319
|
-
} else {
|
|
320
|
-
console.error('Unmapped taxonomic status:', name.taxonomicStatus)
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (name.acceptedNameUsageID) {
|
|
324
|
-
node['dwc:acceptedNameUsageID'] = makeScientificNameUri(name.acceptedNameUsageID)
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (name.parentNameUsageID) {
|
|
328
|
-
node['dwc:parentNameUsageID'] = makeScientificNameUri(name.parentNameUsageID)
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const identifiers = []
|
|
332
|
-
if (name.gbifTaxonID) {
|
|
333
|
-
identifiers.push(makeGbifUri(name.gbifTaxonID))
|
|
334
|
-
}
|
|
335
|
-
if (name.colTaxonID) {
|
|
336
|
-
identifiers.push({ '@id': `https://www.checklistbank.org/dataset/309120/taxon/${name.colTaxonID}` })
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (identifiers.length) {
|
|
340
|
-
node['dwc:taxonID'] = identifiers
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
return node
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function makeLinkedDataForResource (work: catalog.Entity, files: Catalog, resourceId?: ResourceId): NodeObject {
|
|
347
|
-
const resource = new WorkResource({})
|
|
348
|
-
|
|
349
|
-
if (!resourceId) {
|
|
350
|
-
resourceId = `${work.get('id')}:0`
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (files.resources[resourceId] && files.resources[resourceId].metadata.catalog) {
|
|
354
|
-
resource.fields = files.resources[resourceId].metadata.catalog as Record<string, Value>
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (!resource.has('language')) {
|
|
358
|
-
resource.fields.language = work.get('language') as Value
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const node: NodeObject = {
|
|
362
|
-
...makeLinkedDataForWork(resource, files),
|
|
363
|
-
'@id': `${PREFIX}resource/${resourceId}`,
|
|
364
|
-
'@type': 'bibo:DocumentPart',
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const types = resource.get('key_type') ?? work.get('key_type') ?? []
|
|
368
|
-
|
|
369
|
-
if (types.includes('matrix') || types.includes('algorithm')) {
|
|
370
|
-
node['dcterms:type'] = { '@id': 'http://purl.org/dc/dcmitype/Software' }
|
|
371
|
-
} else if (types.includes('key') || types.includes('reference') || types.includes('supplement')) {
|
|
372
|
-
node['dcterms:type'] = { '@id': 'http://purl.org/dc/dcmitype/Text' }
|
|
373
|
-
} else if (types.includes('gallery') || types.includes('collection')) {
|
|
374
|
-
node['dcterms:type'] = { '@id': 'http://purl.org/dc/dcmitype/Collection' }
|
|
375
|
-
} else if (types.includes('checklist')) {
|
|
376
|
-
node['dcterms:type'] = { '@id': 'http://purl.org/dc/dcmitype/Dataset' }
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
if (types.includes('key') || types.includes('matrix')) {
|
|
380
|
-
node['ac:subtype'] = { '@id': 'http://rs.tdwg.org/acsubtype/values/IdentificationKey' }
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const taxonNames = resource.get('taxon') ?? work.get('taxon')
|
|
384
|
-
if (taxonNames) {
|
|
385
|
-
const taxa = mapEntities(taxonNames as string[], files.taxa.entities)
|
|
386
|
-
const coveringTaxon = taxa.length ? getCoveringTaxon(taxa) : null
|
|
387
|
-
if (coveringTaxon !== null) {
|
|
388
|
-
node['ac:taxonCoverage'] = { '@id': coveringTaxon }
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
node['dwc:taxonID'] = taxa.map(taxon => ({ '@id': `${PREFIX}taxon/${taxon.get('id')}` }))
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const scopes = resource.get('scope') ?? work.get('scope')
|
|
395
|
-
if (scopes) {
|
|
396
|
-
for (const scope of scopes as string[]) {
|
|
397
|
-
if (!SCOPES[scope]) {
|
|
398
|
-
console.error('Unmapped scope:', scope)
|
|
399
|
-
continue
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const [property, ...values] = SCOPES[scope]
|
|
403
|
-
|
|
404
|
-
if (!Array.isArray(node[property])) {
|
|
405
|
-
node[property] = []
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
for (const value of values) {
|
|
409
|
-
if (value.startsWith('http')) {
|
|
410
|
-
(node[property] as unknown[]).push({ '@id': value })
|
|
411
|
-
} else {
|
|
412
|
-
(node[property] as unknown[]).push(value)
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
const region = resource.get('region') ?? work.get('region')
|
|
419
|
-
if (region) {
|
|
420
|
-
const places = mapEntities(region as string[], files.places.entities).map(place => ({ '@id': `${PREFIX}place/${place.get('id')}` }))
|
|
421
|
-
node['dcterms:spatial'] = places
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const tags: string[] = []
|
|
425
|
-
|
|
426
|
-
const taxonScopes = resource.get('taxon_scope') ?? work.get('taxon_scope')
|
|
427
|
-
if (taxonScopes) {
|
|
428
|
-
tags.push(...taxonScopes as string[])
|
|
429
|
-
} else if ((resource.get('complete') ?? work.get('complete')) === 'FALSE') {
|
|
430
|
-
tags.push('not intended to be complete')
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const targetTaxa = resource.get('target_taxa') ?? work.get('target_taxa')
|
|
434
|
-
if (targetTaxa) {
|
|
435
|
-
const first = (targetTaxa as string[]).slice(0, -1).map(rank => rank + ',')
|
|
436
|
-
const last = targetTaxa[targetTaxa.length - 1]
|
|
437
|
-
const list = first.length ? first.join('') + ' or ' + last : last
|
|
438
|
-
tags.push(`for identification to ${list}`)
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
if (tags.length) {
|
|
442
|
-
node['ac:tag'] = tags
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
if (files.resources[resourceId].taxa) {
|
|
446
|
-
const leafs = new Set(files.resources[resourceId].taxa.map(taxon => taxon.scientificNameID))
|
|
447
|
-
const taxa = []
|
|
448
|
-
for (const taxon of files.resources[resourceId].taxa) {
|
|
449
|
-
taxa.push(makeLinkedDataForScientificName(taxon))
|
|
450
|
-
|
|
451
|
-
if (taxon.parentNameUsageID) {
|
|
452
|
-
leafs.delete(taxon.parentNameUsageID)
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
node['dcterms:subject'] = taxa
|
|
457
|
-
node['ac:taxonCount'] = leafs.size
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
return node
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
function makeLinkedDataForWork (work: catalog.Entity, files: Catalog): NodeObject {
|
|
464
|
-
const id = work.get('id') as string
|
|
465
|
-
const node: NodeObject = makeWorkUri(id)
|
|
466
|
-
|
|
467
|
-
const languages = work.get('language') as string[]
|
|
468
|
-
node['dcterms:language'] = languages.map(language => ({ '@id': `http://id.loc.gov/vocabulary/iso639-1/${language}` }))
|
|
469
|
-
|
|
470
|
-
if (work.has('title')) {
|
|
471
|
-
const title = work.get('title') as string[]
|
|
472
|
-
if (title.length > languages.length) {
|
|
473
|
-
title.splice(0, title.length, title.join('; '))
|
|
474
|
-
}
|
|
475
|
-
node['dcterms:title'] = title.map((value, i) => ({ '@value': value, '@language': languages[i] }))
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
if (work.has('pages') && (work.get('pages') as string).includes('-')) {
|
|
479
|
-
const containers = work.has('part_of') ? (work.get('part_of') as string[]).map(id => files.catalog.get(id)) : []
|
|
480
|
-
if (containers.find(container => container.get('entry_type') === 'online')) {
|
|
481
|
-
node['@type'] = 'bibo:BookSection'
|
|
482
|
-
} else {
|
|
483
|
-
node['@type'] = 'bibo:AcademicArticle'
|
|
484
|
-
}
|
|
485
|
-
} else if (work.get('entry_type') === 'online') {
|
|
486
|
-
node['@type'] = 'bibo:Website'
|
|
487
|
-
} else {
|
|
488
|
-
node['@type'] = 'bibo:Book'
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
if (work.has('author')) {
|
|
492
|
-
const authors = mapEntities(work.get('author') as string[], files.authors.entities).map(author => ({ '@id': `${PREFIX}author/${author.get('id')}` }))
|
|
493
|
-
node['bibo:authorList'] = { '@list': authors }
|
|
494
|
-
node['dcterms:creator'] = authors
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
if (work.has('url')) {
|
|
498
|
-
const urls = work.get('url') as string[]
|
|
499
|
-
|
|
500
|
-
const handle = urls.find(url => url.startsWith(HANDLE_PREFIX))
|
|
501
|
-
if (handle) {
|
|
502
|
-
node['bibo:handle'] = { '@id': handle.slice(HANDLE_PREFIX.length) }
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
node['schema:url'] = urls.map(url => ({ '@id': url }))
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
if (work.has('fulltext_url')) {
|
|
509
|
-
const urls = work.get('fulltext_url') as string[]
|
|
510
|
-
|
|
511
|
-
node['schema:encoding'] = urls.map(url => ({ '@type': 'schema:MediaObject', 'schema:contentUrl': { '@id': url } }))
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
if (work.has('archive_url')) {
|
|
515
|
-
node['schema:archivedAt'] = (work.get('archive_url') as string[]).map(url => ({ '@id': url }))
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
if (work.has('date')) {
|
|
519
|
-
const date = work.get('date') as string
|
|
520
|
-
let dateType = 'rdfs:Literal'
|
|
521
|
-
if (date.match(/^\d{4}-\d{2}-\d{2}$/)) {
|
|
522
|
-
dateType = 'xsd:date'
|
|
523
|
-
} else if (date.match(/^\d{4}-\d{2}$/)) {
|
|
524
|
-
dateType = 'xsd:gYearMonth'
|
|
525
|
-
} else if (date.match(/^\d{4}$/)) {
|
|
526
|
-
dateType = 'xsd:gYear'
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
node['dcterms:issued'] = { '@value': work.get('date'), '@type': dateType }
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
if (work.has('publisher')) {
|
|
533
|
-
const publishers = mapEntities(work.get('publisher') as string[], files.publishers.entities).map(publisher => ({ '@id': `${PREFIX}publisher/${publisher.get('id')}` }))
|
|
534
|
-
node['dcterms:publisher'] = publishers
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
if (work.has('ISSN')) {
|
|
538
|
-
node['bibo:issn'] = work.get('ISSN')
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
if (work.has('ISBN')) {
|
|
542
|
-
const isbns = work.get('ISBN') as string[]
|
|
543
|
-
|
|
544
|
-
for (const isbn of isbns) {
|
|
545
|
-
if (isbn.length === 13) {
|
|
546
|
-
node['bibo:isbn13'] = isbn
|
|
547
|
-
} else if (isbn.length === 10) {
|
|
548
|
-
node['bibo:isbn10'] = isbn
|
|
549
|
-
} else {
|
|
550
|
-
node['bibo:isbn'] = isbn
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
if (work.has('QID')) {
|
|
556
|
-
node['bibo:uri'] = makeWikidataUri(work.get('QID') as string)
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
if (work.has('DOI')) {
|
|
560
|
-
node['bibo:doi'] = work.get('DOI')
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
if (work.has('volume')) {
|
|
564
|
-
node['bibo:volume'] = work.get('volume')
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
if (work.has('issue')) {
|
|
568
|
-
node['bibo:issue'] = work.get('issue')
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
if (work.has('pages')) {
|
|
572
|
-
const pages = work.get('pages') as string
|
|
573
|
-
const range = pages.split('-')
|
|
574
|
-
|
|
575
|
-
if (pages.match(/^\d+$/)) {
|
|
576
|
-
node['bibo:numPages'] = parseInt(pages)
|
|
577
|
-
} else if (range.length === 2) {
|
|
578
|
-
node['bibo:pages'] = pages
|
|
579
|
-
|
|
580
|
-
const [start, end] = range.map(part => parseInt(part))
|
|
581
|
-
if (!isNaN(start)) {
|
|
582
|
-
node['bibo:pageStart'] = start
|
|
583
|
-
}
|
|
584
|
-
if (!isNaN(end)) {
|
|
585
|
-
node['bibo:pageEnd'] = end
|
|
586
|
-
}
|
|
587
|
-
if (!isNaN(start) && !isNaN(end)) {
|
|
588
|
-
node['bibo:numPages'] = end - start + 1
|
|
589
|
-
}
|
|
590
|
-
} else {
|
|
591
|
-
node['bibo:pages'] = pages
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
if (work.has('edition')) {
|
|
596
|
-
node['bibo:edition'] = work.get('edition')
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
if (work.has('license')) {
|
|
600
|
-
const licenses = work.get('license') as string[]
|
|
601
|
-
node['dcterms:rights'] = licenses.map(license => {
|
|
602
|
-
if (license === '<public domain>' || license.match(/^<.+\?>$/)) {
|
|
603
|
-
return license.slice(1, -1)
|
|
604
|
-
} else {
|
|
605
|
-
return { '@id': `https://spdx.org/licenses/${license}.html` }
|
|
606
|
-
}
|
|
607
|
-
})
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
return node
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
function makeLinkedDataForWorks (files: Catalog): NodeObject[] {
|
|
614
|
-
const nodes: Record<string, NodeObject> = {}
|
|
615
|
-
const works = files.catalog
|
|
616
|
-
|
|
617
|
-
for (const work of works.entities) {
|
|
618
|
-
nodes[work.get('id') as string] = makeLinkedDataForWork(work, files)
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
for (const work of works.entities) {
|
|
622
|
-
const id = work.get('id') as string
|
|
623
|
-
const node = nodes[id]
|
|
624
|
-
const language = work.get('language') as string[]
|
|
625
|
-
|
|
626
|
-
if (work.has('part_of')) {
|
|
627
|
-
const containers = work.get('part_of') as string[]
|
|
628
|
-
|
|
629
|
-
if ((work.get('key_type') as string[]).includes('supplement')) {
|
|
630
|
-
node['bibo:annotates'] = containers.map(makeWorkUri)
|
|
631
|
-
} else {
|
|
632
|
-
node['dcterms:isPartOf'] = containers.map(makeWorkUri)
|
|
633
|
-
|
|
634
|
-
for (const container of containers) {
|
|
635
|
-
if (!Array.isArray(nodes[container]['dcterms:hasPart'])) {
|
|
636
|
-
nodes[container]['dcterms:hasPart'] = []
|
|
637
|
-
}
|
|
638
|
-
(nodes[container]['dcterms:hasPart'] as NodeObject[]).push(makeWorkUri(id))
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
if (work.has('listed_in')) {
|
|
644
|
-
const referers = work.get('listed_in') as string[]
|
|
645
|
-
node['bibo:citedBy'] = node['dcterms:isReferencedBy'] = referers.map(makeWorkUri)
|
|
646
|
-
|
|
647
|
-
for (const referer of referers) {
|
|
648
|
-
nodes[referer]['bibo:cites'] = nodes[referer]['dcterms:references'] = makeWorkUri(id)
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
if (work.has('version_of')) {
|
|
653
|
-
const originals = work.get('version_of') as string[]
|
|
654
|
-
const originalLanguages = originals.map(id => ((works.get(id) as catalog.Entity).get('language') as string[]).join())
|
|
655
|
-
node['bibo:translationOf'] = originals.filter((_, i) => originalLanguages[i] !== language.join()).map(makeWorkUri)
|
|
656
|
-
node['dcterms:isVersionOf'] = originals.filter(id => work.get('id') !== id).map(makeWorkUri)
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
let resourceIndex = 1
|
|
660
|
-
let resourceId
|
|
661
|
-
while ((resourceId = `${id}:${resourceIndex++}`) in files.resources) {
|
|
662
|
-
const resource = makeLinkedDataForResource(work, files, resourceId)
|
|
663
|
-
|
|
664
|
-
if (!Array.isArray(node['dcterms:hasPart'])) {
|
|
665
|
-
node['dcterms:hasPart'] = []
|
|
666
|
-
}
|
|
667
|
-
(node['dcterms:hasPart'] as NodeObject[]).push(resource)
|
|
668
|
-
|
|
669
|
-
if (!Array.isArray(resource['dcterms:isPartOf'])) {
|
|
670
|
-
resource['dcterms:isPartOf'] = []
|
|
671
|
-
}
|
|
672
|
-
(resource['dcterms:isPartOf'] as NodeObject[]).push({ '@id': node['@id'] })
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
return Object.values(nodes)
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
async function writeOutput (document: JsonLdDocument, format = 'jsonld'): Promise<void> {
|
|
680
|
-
if (format === 'jsonld') {
|
|
681
|
-
process.stdout.write(JSON.stringify(document, null, 2))
|
|
682
|
-
return
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
const nquads = await jsonld.toRDF(document, { format: 'application/n-quads' }) as string
|
|
686
|
-
if (format === 'nquads') {
|
|
687
|
-
process.stdout.write(nquads)
|
|
688
|
-
return
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
const parser = new N3.StreamParser()
|
|
692
|
-
const writer = new N3.StreamWriter({ prefixes: PREFIXES })
|
|
693
|
-
parser.write(nquads)
|
|
694
|
-
parser.pipe(writer)
|
|
695
|
-
writer.pipe(process.stdout)
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
async function main (): Promise<void> {
|
|
699
|
-
const args = util.parseArgs({
|
|
700
|
-
allowPositionals: true,
|
|
701
|
-
options: {
|
|
702
|
-
format: {
|
|
703
|
-
type: 'string',
|
|
704
|
-
short: 'f',
|
|
705
|
-
default: 'jsonld',
|
|
706
|
-
}
|
|
707
|
-
},
|
|
708
|
-
})
|
|
709
|
-
const directory = path.resolve(args.positionals[0])
|
|
710
|
-
|
|
711
|
-
const files = {} as Catalog
|
|
712
|
-
for (const sheet of SHEETS) {
|
|
713
|
-
const filePath = path.join(directory, `${sheet}.csv`)
|
|
714
|
-
if (!doesFileExist(filePath)) {
|
|
715
|
-
throw new Error(`File "${sheet}.csv" must be provided`)
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
const file = await fs.readFile(filePath, 'utf8')
|
|
719
|
-
files[sheet] = catalog.loadData(file, sheet)
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
files.resources = {}
|
|
723
|
-
const resources = JSON.parse(await fs.readFile(path.join(directory, 'resources', 'index.json'), 'utf8'))
|
|
724
|
-
for (const id in resources) {
|
|
725
|
-
const filePath = path.join(directory, 'resources', 'dwc', id.split(':').join('-') + '.csv')
|
|
726
|
-
const file = await fs.readFile(filePath, 'utf8')
|
|
727
|
-
const [header, ...rows] = parseCsv(file)
|
|
728
|
-
const taxa = rows.map(row => row.reduce((object, value, index) => {
|
|
729
|
-
object[header[index]] = value
|
|
730
|
-
return object
|
|
731
|
-
}, {} as Record<string, string>) as unknown as AmendedTaxon)
|
|
732
|
-
|
|
733
|
-
files.resources[id] = { metadata: resources[id], taxa }
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
const graph: NodeObject[] = [
|
|
737
|
-
...makeLinkedDataForWorks(files),
|
|
738
|
-
...makeLinkedDataForTaxa(files),
|
|
739
|
-
]
|
|
740
|
-
|
|
741
|
-
for (const entity of files.authors.entities) {
|
|
742
|
-
graph.push(makeLinkedDataForAuthor(entity))
|
|
743
|
-
}
|
|
744
|
-
for (const entity of files.places.entities) {
|
|
745
|
-
graph.push(makeLinkedDataForPlace(entity))
|
|
746
|
-
}
|
|
747
|
-
for (const entity of files.publishers.entities) {
|
|
748
|
-
graph.push(makeLinkedDataForPublisher(entity))
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
const document: JsonLdDocument = {
|
|
752
|
-
'@context': PREFIXES,
|
|
753
|
-
'@graph': graph
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
writeOutput(document, args.values.format)
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
main().catch((error: Error) => {
|
|
760
|
-
console.error(error)
|
|
761
|
-
process.exit(1)
|
|
762
|
-
})
|