@ca-plant-list/ca-plant-list 0.4.11 → 0.4.12
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/data/exceptions.json +16 -1
- package/data/inattaxonphotos.csv +6 -0
- package/data/taxa.csv +1363 -1362
- package/lib/csv.js +37 -1
- package/lib/exceptions.js +2 -2
- package/lib/htmltaxon.js +17 -0
- package/lib/taxa.js +8 -2
- package/lib/taxon.js +24 -6
- package/lib/tools/calflora.js +220 -0
- package/lib/tools/calscape.js +214 -0
- package/lib/tools/taxacsv.js +31 -0
- package/lib/web/pagetaxon.js +5 -1
- package/package.json +4 -3
- package/schemas/exceptions.schema.json +6 -1
- package/scripts/cpl-photos.js +5 -1
- package/scripts/cpl-tools.js +177 -0
- package/types/classes.d.ts +6 -0
package/lib/csv.js
CHANGED
@@ -109,17 +109,53 @@ class CSV {
|
|
109
109
|
return parseSync(content, options);
|
110
110
|
}
|
111
111
|
|
112
|
+
/**
|
113
|
+
* @param {string} fileName
|
114
|
+
* @param {string} [delimiter]
|
115
|
+
* @returns {{headers:string[],data:Object<string,any>[]}}
|
116
|
+
*/
|
117
|
+
static readFileAndHeaders(fileName, delimiter) {
|
118
|
+
let headers;
|
119
|
+
/**
|
120
|
+
* @param {string[]} h
|
121
|
+
*/
|
122
|
+
function getHeaders(h) {
|
123
|
+
headers = h;
|
124
|
+
return h;
|
125
|
+
}
|
126
|
+
|
127
|
+
const content = fs.readFileSync(fileName);
|
128
|
+
const options = this.#getOptions(fileName, getHeaders, delimiter);
|
129
|
+
|
130
|
+
const data = parseSync(content, options);
|
131
|
+
if (headers === undefined) {
|
132
|
+
throw new Error();
|
133
|
+
}
|
134
|
+
return { headers: headers, data: data };
|
135
|
+
}
|
136
|
+
|
112
137
|
/**
|
113
138
|
*
|
114
139
|
* @param {string} fileName
|
115
140
|
* @param {string[][]} data
|
116
141
|
* @param {string[]} [headerData]
|
117
142
|
*/
|
118
|
-
static
|
143
|
+
static writeFileArray(fileName, data, headerData) {
|
119
144
|
const header = headerData ? stringify([headerData]) : "";
|
120
145
|
const content = header + stringify(data);
|
121
146
|
fs.writeFileSync(fileName, content);
|
122
147
|
}
|
148
|
+
|
149
|
+
/**
|
150
|
+
*
|
151
|
+
* @param {string} fileName
|
152
|
+
* @param {Object<string,any>[]} data
|
153
|
+
* @param {string[]} headerData
|
154
|
+
*/
|
155
|
+
static writeFileObject(fileName, data, headerData) {
|
156
|
+
const content = stringify(data, { columns: headerData, header: true });
|
157
|
+
fs.writeFileSync(fileName, content.replaceAll(/,+\n/g, "\n"));
|
158
|
+
}
|
123
159
|
}
|
124
160
|
|
125
161
|
export { CSV };
|
package/lib/exceptions.js
CHANGED
@@ -18,7 +18,7 @@ class Exceptions {
|
|
18
18
|
|
19
19
|
// Read default configuration.
|
20
20
|
this.#exceptions = readConfig(
|
21
|
-
Config.getPackageDir() + "/data/exceptions.json"
|
21
|
+
Config.getPackageDir() + "/data/exceptions.json",
|
22
22
|
);
|
23
23
|
|
24
24
|
// Add/overwrite with local configuration.
|
@@ -38,7 +38,7 @@ class Exceptions {
|
|
38
38
|
* @param {string} name
|
39
39
|
* @param {string} cat
|
40
40
|
* @param {string} subcat
|
41
|
-
* @param {string} defaultValue
|
41
|
+
* @param {string} [defaultValue]
|
42
42
|
*/
|
43
43
|
getValue(name, cat, subcat, defaultValue) {
|
44
44
|
const taxonData = this.#exceptions[name];
|
package/lib/htmltaxon.js
CHANGED
@@ -43,6 +43,23 @@ const DEFAULT_TAXA_COLUMNS = [
|
|
43
43
|
];
|
44
44
|
|
45
45
|
class HTMLTaxon {
|
46
|
+
/**
|
47
|
+
* @param {Taxon} taxon
|
48
|
+
* @returns {string|undefined}
|
49
|
+
*/
|
50
|
+
static getCalscapeLink(taxon) {
|
51
|
+
const calscapeCN = taxon.getCalscapeCommonName();
|
52
|
+
if (!calscapeCN) {
|
53
|
+
return;
|
54
|
+
}
|
55
|
+
return HTML.getLink(
|
56
|
+
`https://www.calscape.org/${taxon.getCalscapeName().replaceAll(" ", "-")}-()`,
|
57
|
+
"Calscape",
|
58
|
+
{},
|
59
|
+
true,
|
60
|
+
);
|
61
|
+
}
|
62
|
+
|
46
63
|
/**
|
47
64
|
* @param {string[]|undefined} colors
|
48
65
|
*/
|
package/lib/taxa.js
CHANGED
@@ -8,6 +8,7 @@ import { Taxon } from "./taxon.js";
|
|
8
8
|
import { Families } from "./families.js";
|
9
9
|
import { FlowerColor } from "./flowercolor.js";
|
10
10
|
import { InatPhoto } from "./inat_photo.js";
|
11
|
+
import { TaxaCSV } from "./tools/taxacsv.js";
|
11
12
|
|
12
13
|
const FLOWER_COLORS = [
|
13
14
|
{ name: "white", color: "white" },
|
@@ -62,8 +63,13 @@ class Taxa {
|
|
62
63
|
|
63
64
|
this.#families = new Families();
|
64
65
|
|
65
|
-
const taxaCSV =
|
66
|
-
this.#loadTaxa(
|
66
|
+
const taxaCSV = new TaxaCSV(dataDir);
|
67
|
+
this.#loadTaxa(
|
68
|
+
taxaCSV.getTaxa(),
|
69
|
+
inclusionList,
|
70
|
+
taxonFactory,
|
71
|
+
showFlowerErrors,
|
72
|
+
);
|
67
73
|
this.#loadTaxa(
|
68
74
|
extraTaxa,
|
69
75
|
inclusionList,
|
package/lib/taxon.js
CHANGED
@@ -22,6 +22,7 @@ class Taxon {
|
|
22
22
|
#iNatID;
|
23
23
|
/**@type {string|undefined} */
|
24
24
|
#iNatSyn;
|
25
|
+
#calscapeCN;
|
25
26
|
#lifeCycle;
|
26
27
|
#flowerColors;
|
27
28
|
#bloomStart;
|
@@ -58,6 +59,8 @@ class Taxon {
|
|
58
59
|
this.#jepsonID = data["jepson id"];
|
59
60
|
this.#calRecNum = data["calrecnum"];
|
60
61
|
this.#iNatID = data["inat id"];
|
62
|
+
this.#calscapeCN =
|
63
|
+
data.calscape_cn === "" ? undefined : data.calscape_cn;
|
61
64
|
this.#lifeCycle = data.life_cycle;
|
62
65
|
const colors = data["flower_color"];
|
63
66
|
this.#flowerColors = colors ? colors.split(",") : undefined;
|
@@ -98,7 +101,7 @@ class Taxon {
|
|
98
101
|
* @param {InatPhoto} photo
|
99
102
|
*/
|
100
103
|
addPhoto(photo) {
|
101
|
-
this.#photos = this.#photos.concat(
|
104
|
+
this.#photos = this.#photos.concat([photo]);
|
102
105
|
}
|
103
106
|
|
104
107
|
getPhotos() {
|
@@ -144,11 +147,26 @@ class Taxon {
|
|
144
147
|
"https://www.calflora.org/app/taxon?crn=" + calfloraID,
|
145
148
|
"Calflora",
|
146
149
|
{},
|
147
|
-
true
|
150
|
+
true,
|
148
151
|
);
|
149
152
|
return this.#cfSyn ? link + " (" + this.#cfSyn + ")" : link;
|
150
153
|
}
|
151
154
|
|
155
|
+
getCalscapeCommonName() {
|
156
|
+
return this.#calscapeCN;
|
157
|
+
}
|
158
|
+
|
159
|
+
getCalscapeName() {
|
160
|
+
return Taxon.getCalscapeName(this.getName());
|
161
|
+
}
|
162
|
+
|
163
|
+
/**
|
164
|
+
* @param {string} name
|
165
|
+
*/
|
166
|
+
static getCalscapeName(name) {
|
167
|
+
return name.replace(" subsp.", " ssp.");
|
168
|
+
}
|
169
|
+
|
152
170
|
getCESA() {
|
153
171
|
return this.#cesa;
|
154
172
|
}
|
@@ -206,7 +224,7 @@ class Taxon {
|
|
206
224
|
const link = HTML.wrap(
|
207
225
|
"span",
|
208
226
|
HTML.getLink(href, this.getName()),
|
209
|
-
attributes
|
227
|
+
attributes,
|
210
228
|
);
|
211
229
|
if (isRare) {
|
212
230
|
return HTML.getToolTip(link, this.getRPIRankAndThreatTooltip(), {
|
@@ -238,7 +256,7 @@ class Taxon {
|
|
238
256
|
"https://www.inaturalist.org/taxa/" + iNatID,
|
239
257
|
"iNaturalist",
|
240
258
|
{},
|
241
|
-
true
|
259
|
+
true,
|
242
260
|
);
|
243
261
|
return this.#iNatSyn ? link + " (" + this.#iNatSyn + ")" : link;
|
244
262
|
}
|
@@ -272,7 +290,7 @@ class Taxon {
|
|
272
290
|
|
273
291
|
getRPIRankAndThreatTooltip() {
|
274
292
|
return RarePlants.getRPIRankAndThreatDescriptions(
|
275
|
-
this.getRPIRankAndThreat()
|
293
|
+
this.getRPIRankAndThreat(),
|
276
294
|
).join("<br>");
|
277
295
|
}
|
278
296
|
|
@@ -285,7 +303,7 @@ class Taxon {
|
|
285
303
|
"https://rareplants.cnps.org/Plants/Details/" + rpiID,
|
286
304
|
"CNPS Rare Plant Inventory",
|
287
305
|
{},
|
288
|
-
true
|
306
|
+
true,
|
289
307
|
);
|
290
308
|
return link;
|
291
309
|
}
|
@@ -0,0 +1,220 @@
|
|
1
|
+
import * as path from "path";
|
2
|
+
import { CSV } from "../csv.js";
|
3
|
+
import { Files } from "../files.js";
|
4
|
+
|
5
|
+
const CALFLORA_URL_ALL =
|
6
|
+
"https://www.calflora.org/app/downtext?xun=117493&table=species&format=Tab&cols=0,1,4,5,8,38,41,43&psp=lifeform::grass,Tree,Herb,Fern,Shrub,Vine!!&par=f&active=";
|
7
|
+
const CALFLORA_URL_COUNTY =
|
8
|
+
"https://www.calflora.org/app/downtext?xun=117493&table=species&format=Tab&cols=0,1,4,5,8,38,41,43&psp=countylist::ALA,CCA!!&active=1";
|
9
|
+
|
10
|
+
/**
|
11
|
+
* @typedef {{
|
12
|
+
* "Native Status":string,
|
13
|
+
* TJMTID:string
|
14
|
+
* "Active in Calflora?":string
|
15
|
+
* Calrecnum:string
|
16
|
+
* }} CalfloraData
|
17
|
+
*/
|
18
|
+
|
19
|
+
class Calflora {
|
20
|
+
/** @type {Object<string,CalfloraData>} */
|
21
|
+
static #taxa = {};
|
22
|
+
|
23
|
+
/**
|
24
|
+
*
|
25
|
+
* @param {string} toolsDataDir
|
26
|
+
* @param {Taxa} taxa
|
27
|
+
* @param {import("../exceptions.js").Exceptions} exceptions
|
28
|
+
* @param {ErrorLog} errorLog
|
29
|
+
*/
|
30
|
+
static async analyze(toolsDataDir, taxa, exceptions, errorLog) {
|
31
|
+
/**
|
32
|
+
* @param {string} url
|
33
|
+
* @param {string} targetFile
|
34
|
+
*/
|
35
|
+
async function retrieveCalfloraFile(url, targetFile) {
|
36
|
+
// Retrieve file if it's not there.
|
37
|
+
targetFile = toolsDataPath + "/" + targetFile;
|
38
|
+
if (Files.exists(targetFile)) {
|
39
|
+
return;
|
40
|
+
}
|
41
|
+
console.log("retrieving " + targetFile);
|
42
|
+
await Files.fetch(url, targetFile);
|
43
|
+
}
|
44
|
+
|
45
|
+
const toolsDataPath = toolsDataDir + "/calflora";
|
46
|
+
// Create data directory if it's not there.
|
47
|
+
Files.mkdir(toolsDataPath);
|
48
|
+
|
49
|
+
const calfloraDataFileNameActive = "calflora_taxa_active.tsv";
|
50
|
+
const calfloraDataFileNameCounties = "calflora_taxa_counties.tsv";
|
51
|
+
|
52
|
+
await retrieveCalfloraFile(
|
53
|
+
CALFLORA_URL_ALL + "1",
|
54
|
+
calfloraDataFileNameActive,
|
55
|
+
);
|
56
|
+
// County list and "all" lists are both incomplete; load everything to get as much as possible.
|
57
|
+
await retrieveCalfloraFile(
|
58
|
+
CALFLORA_URL_COUNTY,
|
59
|
+
calfloraDataFileNameCounties,
|
60
|
+
);
|
61
|
+
|
62
|
+
const csvActive = CSV.readFile(
|
63
|
+
path.join(toolsDataPath, calfloraDataFileNameActive),
|
64
|
+
);
|
65
|
+
const csvCounties = CSV.readFile(
|
66
|
+
path.join(toolsDataPath, calfloraDataFileNameCounties),
|
67
|
+
);
|
68
|
+
|
69
|
+
for (const row of csvActive) {
|
70
|
+
this.#taxa[row["Taxon"]] = row;
|
71
|
+
}
|
72
|
+
for (const row of csvCounties) {
|
73
|
+
this.#taxa[row["Taxon"]] = row;
|
74
|
+
}
|
75
|
+
|
76
|
+
for (const taxon of taxa.getTaxonList()) {
|
77
|
+
const name = taxon.getName();
|
78
|
+
if (name.includes(" unknown")) {
|
79
|
+
continue;
|
80
|
+
}
|
81
|
+
const cfName = taxon.getCalfloraName();
|
82
|
+
const cfData = Calflora.#taxa[cfName];
|
83
|
+
if (!cfData) {
|
84
|
+
if (
|
85
|
+
!exceptions.hasException(name, "calflora", "notintaxondata")
|
86
|
+
) {
|
87
|
+
errorLog.log(name, "not found in Calflora files");
|
88
|
+
}
|
89
|
+
continue;
|
90
|
+
}
|
91
|
+
|
92
|
+
// Check native status.
|
93
|
+
const cfNative = cfData["Native Status"];
|
94
|
+
let cfIsNative = cfNative === "rare" || cfNative === "native";
|
95
|
+
// Override if exception is specified.
|
96
|
+
const nativeException = exceptions.getValue(
|
97
|
+
name,
|
98
|
+
"calflora",
|
99
|
+
"native",
|
100
|
+
undefined,
|
101
|
+
);
|
102
|
+
if (typeof nativeException === "boolean") {
|
103
|
+
if (nativeException === cfIsNative) {
|
104
|
+
errorLog.log(
|
105
|
+
name,
|
106
|
+
"has unnecessary Calflora native override",
|
107
|
+
);
|
108
|
+
}
|
109
|
+
cfIsNative = nativeException;
|
110
|
+
}
|
111
|
+
if (cfIsNative !== taxon.isCANative()) {
|
112
|
+
errorLog.log(
|
113
|
+
name,
|
114
|
+
"has different nativity status in Calflora",
|
115
|
+
cfIsNative.toString(),
|
116
|
+
);
|
117
|
+
}
|
118
|
+
|
119
|
+
// Check if it is active in Calflora.
|
120
|
+
const isActive = cfData["Active in Calflora?"];
|
121
|
+
if (isActive !== "YES") {
|
122
|
+
errorLog.log(name, "is not active in Calflora", isActive);
|
123
|
+
}
|
124
|
+
|
125
|
+
// Check Jepson IDs.
|
126
|
+
const cfJepsonID = cfData.TJMTID;
|
127
|
+
if (cfJepsonID !== taxon.getJepsonID()) {
|
128
|
+
if (
|
129
|
+
!exceptions.hasException(name, "calflora", "badjepsonid") &&
|
130
|
+
!exceptions.hasException(name, "calflora", "notintaxondata")
|
131
|
+
) {
|
132
|
+
errorLog.log(
|
133
|
+
name,
|
134
|
+
"Jepson ID in Calflora is different than taxa.csv",
|
135
|
+
cfJepsonID,
|
136
|
+
taxon.getJepsonID(),
|
137
|
+
);
|
138
|
+
}
|
139
|
+
}
|
140
|
+
|
141
|
+
// Check Calflora ID.
|
142
|
+
const cfID = cfData["Calrecnum"];
|
143
|
+
if (cfID !== taxon.getCalfloraID()) {
|
144
|
+
errorLog.log(
|
145
|
+
name,
|
146
|
+
"Calflora ID in Calflora is different than taxa.csv",
|
147
|
+
cfID,
|
148
|
+
taxon.getCalfloraID(),
|
149
|
+
);
|
150
|
+
}
|
151
|
+
}
|
152
|
+
|
153
|
+
this.#checkExceptions(taxa, exceptions, errorLog);
|
154
|
+
}
|
155
|
+
|
156
|
+
/**
|
157
|
+
* @param {Taxa} taxa
|
158
|
+
* @param {import("../exceptions.js").Exceptions} exceptions
|
159
|
+
* @param {ErrorLog} errorLog
|
160
|
+
*/
|
161
|
+
static #checkExceptions(taxa, exceptions, errorLog) {
|
162
|
+
// Check the Calflora exceptions and make sure they still apply.
|
163
|
+
for (const [name, v] of exceptions.getExceptions()) {
|
164
|
+
const exceptions = v.calflora;
|
165
|
+
if (!exceptions) {
|
166
|
+
continue;
|
167
|
+
}
|
168
|
+
|
169
|
+
// Make sure the taxon is still in our list.
|
170
|
+
const taxon = taxa.getTaxon(name);
|
171
|
+
if (!taxon) {
|
172
|
+
// Don't process global exceptions if taxon is not in local list.
|
173
|
+
if (taxa.isSubset() && !v.local) {
|
174
|
+
continue;
|
175
|
+
}
|
176
|
+
errorLog.log(
|
177
|
+
name,
|
178
|
+
"has Calflora exceptions but not in Taxa collection",
|
179
|
+
);
|
180
|
+
continue;
|
181
|
+
}
|
182
|
+
|
183
|
+
for (const [k] of Object.entries(exceptions)) {
|
184
|
+
const cfData = Calflora.#taxa[name];
|
185
|
+
switch (k) {
|
186
|
+
case "badjepsonid": {
|
187
|
+
// Make sure Jepson ID is still wrong.
|
188
|
+
const cfID = cfData.TJMTID;
|
189
|
+
const jepsID = taxon.getJepsonID();
|
190
|
+
if (cfID === jepsID) {
|
191
|
+
errorLog.log(
|
192
|
+
name,
|
193
|
+
"has Calflora badjepsonid exception but IDs are the same",
|
194
|
+
);
|
195
|
+
}
|
196
|
+
break;
|
197
|
+
}
|
198
|
+
case "native":
|
199
|
+
break;
|
200
|
+
case "notintaxondata":
|
201
|
+
if (cfData) {
|
202
|
+
errorLog.log(
|
203
|
+
name,
|
204
|
+
"found in Calflora data but has notintaxondata exception",
|
205
|
+
);
|
206
|
+
}
|
207
|
+
break;
|
208
|
+
default:
|
209
|
+
errorLog.log(
|
210
|
+
name,
|
211
|
+
"unrecognized Calflora exception",
|
212
|
+
k,
|
213
|
+
);
|
214
|
+
}
|
215
|
+
}
|
216
|
+
}
|
217
|
+
}
|
218
|
+
}
|
219
|
+
|
220
|
+
export { Calflora };
|
@@ -0,0 +1,214 @@
|
|
1
|
+
import path from "node:path";
|
2
|
+
import xlsx from "exceljs";
|
3
|
+
import { Files } from "../files.js";
|
4
|
+
import { TaxaCSV } from "./taxacsv.js";
|
5
|
+
import { Taxon } from "../taxon.js";
|
6
|
+
|
7
|
+
export class Calscape {
|
8
|
+
/**
|
9
|
+
* @param {string} toolsDataDir
|
10
|
+
* @param {string} dataDir
|
11
|
+
* @param {Taxa} taxa
|
12
|
+
* @param {import("../exceptions.js").Exceptions} exceptions
|
13
|
+
* @param {ErrorLog} errorLog
|
14
|
+
* @param {boolean} update
|
15
|
+
*/
|
16
|
+
static async analyze(
|
17
|
+
toolsDataDir,
|
18
|
+
dataDir,
|
19
|
+
taxa,
|
20
|
+
exceptions,
|
21
|
+
errorLog,
|
22
|
+
update,
|
23
|
+
) {
|
24
|
+
const calscapeData = await getCalscapeData(toolsDataDir);
|
25
|
+
|
26
|
+
for (const taxon of taxa.getTaxonList()) {
|
27
|
+
const taxonName = taxon.getName();
|
28
|
+
const taxonCN = taxon.getCalscapeCommonName();
|
29
|
+
const calscapeCN = getCalscapeCommonName(
|
30
|
+
taxonName,
|
31
|
+
calscapeData,
|
32
|
+
exceptions,
|
33
|
+
);
|
34
|
+
|
35
|
+
if (taxonCN !== calscapeCN) {
|
36
|
+
errorLog.log(
|
37
|
+
taxonName,
|
38
|
+
"name in Calscape data is different than taxa.csv",
|
39
|
+
calscapeCN ?? "undefined",
|
40
|
+
taxonCN ?? "undefined",
|
41
|
+
);
|
42
|
+
}
|
43
|
+
// Calscape should only have natives, so make sure it's recorded as native.
|
44
|
+
if (calscapeCN && !taxon.isCANative()) {
|
45
|
+
errorLog.log(
|
46
|
+
taxonName,
|
47
|
+
"is in Calscape but not native in taxa.csv",
|
48
|
+
);
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
checkExceptions(taxa, exceptions, errorLog);
|
53
|
+
|
54
|
+
if (update) {
|
55
|
+
updateTaxaCSV(dataDir, calscapeData, exceptions);
|
56
|
+
}
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
/**
|
61
|
+
* @param {Taxa} taxa
|
62
|
+
* @param {import("../exceptions.js").Exceptions} exceptions
|
63
|
+
* @param {ErrorLog} errorLog
|
64
|
+
*/
|
65
|
+
function checkExceptions(taxa, exceptions, errorLog) {
|
66
|
+
// Check the Calscape exceptions and make sure they still apply.
|
67
|
+
for (const [name, v] of exceptions.getExceptions()) {
|
68
|
+
const exceptions = v.calscape;
|
69
|
+
if (!exceptions) {
|
70
|
+
continue;
|
71
|
+
}
|
72
|
+
|
73
|
+
// Make sure the taxon is still in our list.
|
74
|
+
const taxon = taxa.getTaxon(name);
|
75
|
+
if (!taxon) {
|
76
|
+
// Don't process global exceptions if taxon is not in local list.
|
77
|
+
if (taxa.isSubset() && !v.local) {
|
78
|
+
continue;
|
79
|
+
}
|
80
|
+
errorLog.log(
|
81
|
+
name,
|
82
|
+
"has Calscape exceptions but not in Taxa collection",
|
83
|
+
);
|
84
|
+
continue;
|
85
|
+
}
|
86
|
+
|
87
|
+
for (const [k] of Object.entries(exceptions)) {
|
88
|
+
switch (k) {
|
89
|
+
case "notnative": {
|
90
|
+
if (taxon.isCANative()) {
|
91
|
+
errorLog.log(
|
92
|
+
name,
|
93
|
+
"has Calscape notnative exception but is native in taxa.csv",
|
94
|
+
);
|
95
|
+
}
|
96
|
+
break;
|
97
|
+
}
|
98
|
+
default:
|
99
|
+
errorLog.log(name, "unrecognized Calscape exception", k);
|
100
|
+
}
|
101
|
+
}
|
102
|
+
}
|
103
|
+
}
|
104
|
+
|
105
|
+
/**
|
106
|
+
* @param {string} taxonName
|
107
|
+
* @param {Map<string,string>} calscapeData
|
108
|
+
* @param {import("../exceptions.js").Exceptions} exceptions
|
109
|
+
* @returns {string|undefined}
|
110
|
+
*/
|
111
|
+
function getCalscapeCommonName(taxonName, calscapeData, exceptions) {
|
112
|
+
const calscapeCN = calscapeData.get(Taxon.getCalscapeName(taxonName));
|
113
|
+
if (
|
114
|
+
calscapeCN &&
|
115
|
+
exceptions.hasException(taxonName, "calscape", "notnative")
|
116
|
+
) {
|
117
|
+
return;
|
118
|
+
}
|
119
|
+
return calscapeCN;
|
120
|
+
}
|
121
|
+
|
122
|
+
/**
|
123
|
+
* @param {string} toolsDataDir
|
124
|
+
* @returns {Promise<Map<string,string>>}
|
125
|
+
*/
|
126
|
+
async function getCalscapeData(toolsDataDir) {
|
127
|
+
/**
|
128
|
+
* @param {import("exceljs").Cell} cell
|
129
|
+
*/
|
130
|
+
function getCellValue(cell) {
|
131
|
+
const value = cell.value;
|
132
|
+
if (value === null || value === undefined) {
|
133
|
+
return undefined;
|
134
|
+
}
|
135
|
+
return value.toString();
|
136
|
+
}
|
137
|
+
|
138
|
+
toolsDataDir = path.join(toolsDataDir, "calscape");
|
139
|
+
Files.mkdir(toolsDataDir);
|
140
|
+
await retrieveCalscapeFile(toolsDataDir);
|
141
|
+
|
142
|
+
/** @type {Map<string,string>} */
|
143
|
+
const data = new Map();
|
144
|
+
|
145
|
+
const wb = new xlsx.Workbook();
|
146
|
+
await wb.xlsx.readFile(getExcelFilename(toolsDataDir)).then(function () {
|
147
|
+
const ws = wb.worksheets[0];
|
148
|
+
let isInData = false;
|
149
|
+
for (let index = 0; index < ws.rowCount; index++) {
|
150
|
+
const row = ws.getRow(index);
|
151
|
+
const col1 = getCellValue(row.getCell(1));
|
152
|
+
if (!isInData) {
|
153
|
+
if (col1 === "Botanical Name") {
|
154
|
+
isInData = true;
|
155
|
+
}
|
156
|
+
continue;
|
157
|
+
}
|
158
|
+
const col2 = getCellValue(row.getCell(2));
|
159
|
+
if (!col1 || !col2) {
|
160
|
+
continue;
|
161
|
+
}
|
162
|
+
data.set(col1, col2);
|
163
|
+
}
|
164
|
+
});
|
165
|
+
|
166
|
+
return data;
|
167
|
+
}
|
168
|
+
|
169
|
+
/**
|
170
|
+
* @param {string} toolsDataDir
|
171
|
+
*/
|
172
|
+
function getExcelFilename(toolsDataDir) {
|
173
|
+
return path.join(toolsDataDir, "calscape.xlsx");
|
174
|
+
}
|
175
|
+
|
176
|
+
/**
|
177
|
+
* @param {string} toolsDataDir
|
178
|
+
*/
|
179
|
+
async function retrieveCalscapeFile(toolsDataDir) {
|
180
|
+
// Retrieve file if it's not there.
|
181
|
+
const targetFile = getExcelFilename(toolsDataDir);
|
182
|
+
if (Files.exists(targetFile)) {
|
183
|
+
return;
|
184
|
+
}
|
185
|
+
console.info("retrieving " + targetFile);
|
186
|
+
await Files.fetch("https://www.calscape.org/export/search/", targetFile);
|
187
|
+
}
|
188
|
+
|
189
|
+
/**
|
190
|
+
* @param {string} dataDir
|
191
|
+
* @param {Map<string,string>} calscapeData
|
192
|
+
* @param {import("../exceptions.js").Exceptions} exceptions
|
193
|
+
*/
|
194
|
+
function updateTaxaCSV(dataDir, calscapeData, exceptions) {
|
195
|
+
const taxa = new TaxaCSV(dataDir);
|
196
|
+
|
197
|
+
for (const taxonData of taxa.getTaxa()) {
|
198
|
+
const taxonCN = taxonData.calscape_cn;
|
199
|
+
const calscapeCN = getCalscapeCommonName(
|
200
|
+
taxonData.taxon_name,
|
201
|
+
calscapeData,
|
202
|
+
exceptions,
|
203
|
+
);
|
204
|
+
if (taxonCN !== calscapeCN) {
|
205
|
+
if (calscapeCN === undefined) {
|
206
|
+
delete taxonData.calscape_cn;
|
207
|
+
} else {
|
208
|
+
taxonData.calscape_cn = calscapeCN;
|
209
|
+
}
|
210
|
+
}
|
211
|
+
}
|
212
|
+
|
213
|
+
taxa.write();
|
214
|
+
}
|
@@ -0,0 +1,31 @@
|
|
1
|
+
import path from "path";
|
2
|
+
import { CSV } from "../csv.js";
|
3
|
+
|
4
|
+
export class TaxaCSV {
|
5
|
+
#filePath;
|
6
|
+
#headers;
|
7
|
+
/** @type {TaxonData[]} */
|
8
|
+
#taxa;
|
9
|
+
|
10
|
+
/**
|
11
|
+
* @param {string} dataDir
|
12
|
+
*/
|
13
|
+
constructor(dataDir) {
|
14
|
+
this.#filePath = path.join(dataDir, "taxa.csv");
|
15
|
+
const csv = CSV.readFileAndHeaders(this.#filePath);
|
16
|
+
// @ts-ignore
|
17
|
+
this.#taxa = csv.data;
|
18
|
+
this.#headers = csv.headers;
|
19
|
+
}
|
20
|
+
|
21
|
+
/**
|
22
|
+
* @returns {TaxonData[]}
|
23
|
+
*/
|
24
|
+
getTaxa() {
|
25
|
+
return this.#taxa;
|
26
|
+
}
|
27
|
+
|
28
|
+
write() {
|
29
|
+
CSV.writeFileObject(this.#filePath, this.#taxa, this.#headers);
|
30
|
+
}
|
31
|
+
}
|
package/lib/web/pagetaxon.js
CHANGED
@@ -36,6 +36,10 @@ class PageTaxon extends GenericPage {
|
|
36
36
|
if (iNatLink) {
|
37
37
|
links.push(iNatLink);
|
38
38
|
}
|
39
|
+
const calscapeLink = HTMLTaxon.getCalscapeLink(this.#taxon);
|
40
|
+
if (calscapeLink) {
|
41
|
+
links.push(calscapeLink);
|
42
|
+
}
|
39
43
|
const rpiLink = this.#taxon.getRPITaxonLink();
|
40
44
|
if (rpiLink) {
|
41
45
|
links.push(rpiLink);
|
@@ -120,7 +124,7 @@ class PageTaxon extends GenericPage {
|
|
120
124
|
}
|
121
125
|
|
122
126
|
return HTML.wrap("div", "<ul>" + HTML.arrayToLI(ranks) + "</ul>", {
|
123
|
-
class: "section",
|
127
|
+
class: "section nobullet",
|
124
128
|
});
|
125
129
|
}
|
126
130
|
|