@ca-plant-list/ca-plant-list 0.4.21 → 0.4.23

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.
Files changed (47) hide show
  1. package/data/exceptions.json +22 -1
  2. package/data/synonyms.csv +2 -0
  3. package/data/taxa.csv +1754 -1753
  4. package/lib/basepagerenderer.js +10 -4
  5. package/lib/ebook/images.js +3 -3
  6. package/lib/ebook/pages/{page_list_families.js → pageListFamilies.js} +1 -1
  7. package/lib/ebook/pages/{page_list_flowers.js → pageListFlowers.js} +2 -2
  8. package/lib/ebook/pages/page_list_species.js +1 -1
  9. package/lib/ebook/pages/taxonpage.js +1 -1
  10. package/lib/ebook/pages/tocpage.js +2 -2
  11. package/lib/ebook/plantbook.js +3 -3
  12. package/lib/errorlog.js +1 -1
  13. package/lib/externalsites.js +113 -35
  14. package/lib/files.js +3 -5
  15. package/lib/flowercolor.js +2 -2
  16. package/lib/genera.js +4 -4
  17. package/lib/html.js +7 -8
  18. package/lib/htmltaxon.js +122 -33
  19. package/lib/index.d.ts +72 -27
  20. package/lib/index.js +3 -3
  21. package/lib/pagerenderer.js +6 -6
  22. package/lib/taxonomy/families.js +104 -0
  23. package/lib/{taxa.js → taxonomy/taxa.js} +18 -18
  24. package/lib/{taxon.js → taxonomy/taxon.js} +41 -111
  25. package/lib/taxonomy/taxonomy.js +17 -0
  26. package/lib/tools/calflora.js +2 -2
  27. package/lib/tools/calscape.js +3 -3
  28. package/lib/tools/cch2.js +128 -10
  29. package/lib/tools/fna.js +163 -0
  30. package/lib/tools/inat.js +3 -3
  31. package/lib/tools/jepsoneflora.js +21 -2
  32. package/lib/tools/rpi.js +5 -5
  33. package/lib/tools/supplementaltext.js +1 -1
  34. package/lib/tools/taxacsv.js +23 -4
  35. package/lib/types.js +10 -0
  36. package/lib/utils/inat-tools.js +2 -2
  37. package/lib/web/pageFamily.js +146 -0
  38. package/lib/web/pagetaxon.js +21 -63
  39. package/package.json +2 -1
  40. package/scripts/build-ebook.js +4 -4
  41. package/scripts/build-site.js +3 -3
  42. package/scripts/cpl-photos.js +1 -1
  43. package/scripts/cpl-tools.js +18 -2
  44. package/scripts/inatobsphotos.js +2 -2
  45. package/scripts/inattaxonphotos.js +2 -2
  46. package/lib/families.js +0 -243
  47. package/lib/jepson.js +0 -17
@@ -1,20 +1,17 @@
1
- import { HTML } from "./html.js";
2
- import { RarePlants } from "./rareplants.js";
1
+ import { Taxonomy } from "./taxonomy.js";
3
2
 
4
- class Taxon {
3
+ class Taxon extends Taxonomy {
5
4
  #genera;
6
5
  #name;
7
6
  #genus;
8
7
  #commonNames;
9
8
  #status;
10
- #jepsonID;
11
9
  #calRecNum;
12
- /**@type {string|undefined} */
13
- #cfSyn;
14
10
  #iNatID;
15
11
  /**@type {string|undefined} */
16
12
  #iNatSyn;
17
13
  #cch2id;
14
+ #fnaName;
18
15
  #calscapeCN;
19
16
  #lifeCycle;
20
17
  #flowerColors;
@@ -28,14 +25,15 @@ class Taxon {
28
25
  #rankGlobal;
29
26
  /** @type {string[]} */
30
27
  #synonyms = [];
31
- /** @type {import("./photo.js").Photo[]}*/
28
+ /** @type {import("../photo.js").Photo[]}*/
32
29
  #photos = [];
33
30
 
34
31
  /**
35
- * @param {import("./index.js").TaxonData} data
36
- * @param {import("./genera.js").Genera} genera
32
+ * @param {import("../index.js").TaxonData} data
33
+ * @param {import("../genera.js").Genera} genera
37
34
  */
38
35
  constructor(data, genera) {
36
+ super(data);
39
37
  this.#genera = genera;
40
38
  const name = data["taxon_name"];
41
39
  const commonNames = data["common name"];
@@ -49,10 +47,10 @@ class Taxon {
49
47
  ? commonNames.split(",").map((t) => t.trim())
50
48
  : [];
51
49
  this.#status = data["status"];
52
- this.#jepsonID = data["jepson id"];
53
50
  this.#calRecNum = data["calrecnum"];
54
51
  this.#iNatID = data["inat id"];
55
52
  this.#cch2id = data.cch2_id;
53
+ this.#fnaName = data.fna ?? "";
56
54
  this.#calscapeCN =
57
55
  data.calscape_cn === "" ? undefined : data.calscape_cn;
58
56
  this.#lifeCycle = data.life_cycle;
@@ -80,10 +78,6 @@ class Taxon {
80
78
  addSynonym(syn, type) {
81
79
  this.#synonyms.push(syn);
82
80
  switch (type) {
83
- case "CF":
84
- // Synonym is in Calflora format.
85
- this.#cfSyn = syn;
86
- break;
87
81
  case "INAT":
88
82
  // Synonyms should be in Jepson format, but store iNatName in iNat format (no var or subsp, space after x).
89
83
  this.#iNatSyn = syn;
@@ -92,14 +86,14 @@ class Taxon {
92
86
  }
93
87
 
94
88
  /**
95
- * @param {import("./photo.js").Photo} photo
89
+ * @param {import("../photo.js").Photo} photo
96
90
  */
97
91
  addPhoto(photo) {
98
92
  this.#photos = this.#photos.concat([photo]);
99
93
  }
100
94
 
101
95
  /**
102
- * @returns {import("./photo.js").Photo[]}
96
+ * @returns {import("../photo.js").Photo[]}
103
97
  */
104
98
  getPhotos() {
105
99
  return this.#photos;
@@ -125,9 +119,6 @@ class Taxon {
125
119
  }
126
120
 
127
121
  getCalfloraName() {
128
- if (this.#cfSyn) {
129
- return this.#cfSyn;
130
- }
131
122
  return this.getName().replace(" subsp.", " ssp.").replace("×", "X");
132
123
  }
133
124
 
@@ -135,20 +126,6 @@ class Taxon {
135
126
  return this.#calRecNum;
136
127
  }
137
128
 
138
- getCalfloraTaxonLink() {
139
- const calfloraID = this.getCalfloraID();
140
- if (!calfloraID) {
141
- return;
142
- }
143
- const link = HTML.getLink(
144
- "https://www.calflora.org/app/taxon?crn=" + calfloraID,
145
- "Calflora",
146
- {},
147
- true,
148
- );
149
- return this.#cfSyn ? link + " (" + this.#cfSyn + ")" : link;
150
- }
151
-
152
129
  getCalscapeCommonName() {
153
130
  return this.#calscapeCN;
154
131
  }
@@ -171,12 +148,18 @@ class Taxon {
171
148
  return this.#cch2id;
172
149
  }
173
150
 
151
+ /**
152
+ * @returns {string}
153
+ */
174
154
  getCESA() {
175
- return this.#cesa;
155
+ return this.#cesa ?? "";
176
156
  }
177
157
 
158
+ /**
159
+ * @returns {string}
160
+ */
178
161
  getCNDDBRank() {
179
- return this.#rankCNDDB;
162
+ return this.#rankCNDDB ?? "";
180
163
  }
181
164
 
182
165
  getCommonNames() {
@@ -187,14 +170,27 @@ class Taxon {
187
170
  return this.getGenus().getFamily();
188
171
  }
189
172
 
173
+ /**
174
+ * @returns {string}
175
+ */
190
176
  getFESA() {
191
- return this.#fesa;
177
+ return this.#fesa ?? "";
192
178
  }
193
179
 
194
180
  getFileName(ext = "html") {
195
181
  return this.getBaseFileName() + "." + ext;
196
182
  }
197
183
 
184
+ /**
185
+ * @returns {string}
186
+ */
187
+ getFNAName() {
188
+ if (this.#fnaName === "true") {
189
+ return this.getName();
190
+ }
191
+ return this.#fnaName;
192
+ }
193
+
198
194
  getFlowerColors() {
199
195
  return this.#flowerColors;
200
196
  }
@@ -207,35 +203,11 @@ class Taxon {
207
203
  return this.#genus;
208
204
  }
209
205
 
210
- getGlobalRank() {
211
- return this.#rankGlobal;
212
- }
213
-
214
206
  /**
215
- *
216
- * @param {boolean|string|undefined} href
217
- * @param {boolean} includeRPI
207
+ * @returns {string}
218
208
  */
219
- getHTMLLink(href = true, includeRPI = true) {
220
- href = href ? "./" + this.getFileName() : undefined;
221
- let className = this.isNative() ? "native" : "non-native";
222
- let isRare = false;
223
- if (includeRPI && this.isRare()) {
224
- isRare = true;
225
- className += " rare";
226
- }
227
- const attributes = { class: className };
228
- const link = HTML.wrap(
229
- "span",
230
- HTML.getLink(href, this.getName()),
231
- attributes,
232
- );
233
- if (isRare) {
234
- return HTML.getToolTip(link, this.getRPIRankAndThreatTooltip(), {
235
- icon: false,
236
- });
237
- }
238
- return link;
209
+ getGlobalRank() {
210
+ return this.#rankGlobal ?? "";
239
211
  }
240
212
 
241
213
  getINatID() {
@@ -254,27 +226,6 @@ class Taxon {
254
226
  return this.#iNatSyn;
255
227
  }
256
228
 
257
- getINatTaxonLink() {
258
- const iNatID = this.getINatID();
259
- if (!iNatID) {
260
- return "";
261
- }
262
- const link = HTML.getLink(
263
- "https://www.inaturalist.org/taxa/" + iNatID,
264
- "iNaturalist",
265
- {},
266
- true,
267
- );
268
- return this.#iNatSyn ? link + " (" + this.#iNatSyn + ")" : link;
269
- }
270
-
271
- /**
272
- * @returns {string}
273
- */
274
- getJepsonID() {
275
- return this.#jepsonID;
276
- }
277
-
278
229
  getLifeCycle() {
279
230
  return this.#lifeCycle;
280
231
  }
@@ -287,6 +238,9 @@ class Taxon {
287
238
  return this.#rpiID;
288
239
  }
289
240
 
241
+ /**
242
+ * @returns {string}
243
+ */
290
244
  getRPIRank() {
291
245
  if (!this.#rankRPI) {
292
246
  return this.#rankRPI;
@@ -301,38 +255,12 @@ class Taxon {
301
255
  return this.#rankRPI;
302
256
  }
303
257
 
304
- /**
305
- * @deprecated
306
- */
307
- getRPIRankAndThreatTooltip() {
308
- return RarePlants.getRPIRankAndThreatDescriptions(
309
- this.getRPIRankAndThreat(),
310
- ).join("<br>");
311
- }
312
-
313
- /**
314
- * @deprecated
315
- */
316
- getRPITaxonLink() {
317
- const rpiID = this.getRPIID();
318
- if (!rpiID) {
319
- return "";
320
- }
321
- const link = HTML.getLink(
322
- "https://rareplants.cnps.org/Plants/Details/" + rpiID,
323
- "CNPS Rare Plant Inventory",
324
- {},
325
- true,
326
- );
327
- return link;
328
- }
329
-
330
258
  getStatus() {
331
259
  return this.#status;
332
260
  }
333
261
 
334
262
  /**
335
- * @param {import("./config.js").Config} config
263
+ * @param {import("../config.js").Config} config
336
264
  * @returns {string}
337
265
  */
338
266
  getStatusDescription(config) {
@@ -341,6 +269,8 @@ class Taxon {
341
269
  return "Native";
342
270
  case "NC":
343
271
  return config.getLabel("status-NC", "Introduced");
272
+ case "U":
273
+ return "Nativity Uncertain";
344
274
  case "X":
345
275
  return "Introduced";
346
276
  }
@@ -0,0 +1,17 @@
1
+ export class Taxonomy {
2
+ #data;
3
+
4
+ /**
5
+ * @param {import("../index.js").TaxonomyData} data
6
+ */
7
+ constructor(data) {
8
+ this.#data = data;
9
+ }
10
+
11
+ /**
12
+ * @returns {string}
13
+ */
14
+ getJepsonID() {
15
+ return this.#data["jepson id"];
16
+ }
17
+ }
@@ -25,7 +25,7 @@ export class Calflora {
25
25
  /**
26
26
  * @param {string} toolsDataDir
27
27
  * @param {string} dataDir
28
- * @param {import("../taxa.js").Taxa} taxa
28
+ * @param {import("../types.js").Taxa} taxa
29
29
  * @param {import("../exceptions.js").Exceptions} exceptions
30
30
  * @param {import("../errorlog.js").ErrorLog} errorLog
31
31
  * @param {boolean} update
@@ -175,7 +175,7 @@ export class Calflora {
175
175
  }
176
176
 
177
177
  /**
178
- * @param {import("../taxa.js").Taxa} taxa
178
+ * @param {import("../types.js").Taxa} taxa
179
179
  * @param {import("../exceptions.js").Exceptions} exceptions
180
180
  * @param {import("../errorlog.js").ErrorLog} errorLog
181
181
  */
@@ -2,13 +2,13 @@ import path from "node:path";
2
2
  import xlsx from "exceljs";
3
3
  import { Files } from "../files.js";
4
4
  import { TaxaCSV } from "./taxacsv.js";
5
- import { Taxon } from "../taxon.js";
5
+ import { Taxon } from "../taxonomy/taxon.js";
6
6
 
7
7
  export class Calscape {
8
8
  /**
9
9
  * @param {string} toolsDataDir
10
10
  * @param {string} dataDir
11
- * @param {import("../taxa.js").Taxa} taxa
11
+ * @param {import("../types.js").Taxa} taxa
12
12
  * @param {import("../exceptions.js").Exceptions} exceptions
13
13
  * @param {import("../errorlog.js").ErrorLog} errorLog
14
14
  * @param {boolean} update
@@ -58,7 +58,7 @@ export class Calscape {
58
58
  }
59
59
 
60
60
  /**
61
- * @param {import("../taxa.js").Taxa} taxa
61
+ * @param {import("../types.js").Taxa} taxa
62
62
  * @param {import("../exceptions.js").Exceptions} exceptions
63
63
  * @param {import("../errorlog.js").ErrorLog} errorLog
64
64
  */
package/lib/tools/cch2.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import path from "node:path";
2
2
  import { CSV } from "../csv.js";
3
3
  import { TaxaCSV } from "./taxacsv.js";
4
+ import { Files } from "../files.js";
5
+ import puppeteer from "puppeteer";
6
+ import { renameSync } from "node:fs";
4
7
 
5
8
  /**
6
9
  * @typedef {{id:string}} CCHTaxon
@@ -11,42 +14,99 @@ export class CCH2 {
11
14
  /**
12
15
  * @param {string} toolsDataDir
13
16
  * @param {string} dataDir
14
- * @param {import("../taxa.js").Taxa} taxa
17
+ * @param {import("../exceptions.js").Exceptions} exceptions
18
+ * @param {import("../types.js").Taxa} taxa
15
19
  * @param {import("../errorlog.js").ErrorLog} errorLog
16
20
  * @param {boolean} update
17
21
  */
18
- static async analyze(toolsDataDir, dataDir, taxa, errorLog, update) {
22
+ static async analyze(
23
+ toolsDataDir,
24
+ dataDir,
25
+ exceptions,
26
+ taxa,
27
+ errorLog,
28
+ update,
29
+ ) {
19
30
  const toolsDataPath = path.join(toolsDataDir, "cch2");
20
31
 
21
32
  const cchTaxa = await getCCHTaxa(toolsDataPath, taxa);
22
33
 
23
34
  const idsToUpdate = new Map();
24
35
  for (const taxon of taxa.getTaxonList()) {
25
- const cchTaxon = cchTaxa.get(taxon.getName());
36
+ const name = taxon.getName();
37
+ const cchTaxon = cchTaxa.get(name);
26
38
  if (!cchTaxon) {
27
- errorLog.log(taxon.getName(), "not found in CCH data");
39
+ if (!exceptions.hasException(name, "cch", "notincch")) {
40
+ errorLog.log(name, "not found in CCH data");
41
+ }
28
42
  continue;
29
43
  }
30
44
  if (cchTaxon.id !== taxon.getCCH2ID()) {
31
45
  errorLog.log(
32
- taxon.getName(),
46
+ name,
33
47
  "id in CCH data does not match id in taxa.csv",
34
48
  cchTaxon.id,
35
49
  taxon.getCCH2ID(),
36
50
  );
37
- idsToUpdate.set(taxon.getName(), cchTaxon.id);
51
+ idsToUpdate.set(name, cchTaxon.id);
38
52
  }
39
53
  }
40
54
 
55
+ this.#checkExceptions(exceptions, taxa, errorLog, cchTaxa);
56
+
41
57
  if (update) {
42
58
  updateTaxaCSV(dataDir, idsToUpdate);
43
59
  }
44
60
  }
61
+
62
+ /**
63
+ * @param {import("../exceptions.js").Exceptions} exceptions
64
+ * @param {import("../types.js").Taxa} taxa
65
+ * @param {import("../errorlog.js").ErrorLog} errorLog
66
+ * @param {CCHTaxa} cchTaxa
67
+ */
68
+ static #checkExceptions(exceptions, taxa, errorLog, cchTaxa) {
69
+ // Check the CCH exceptions and make sure they still apply.
70
+ for (const [name, v] of exceptions.getExceptions()) {
71
+ const exceptions = v.cch;
72
+ if (!exceptions) {
73
+ continue;
74
+ }
75
+
76
+ // Make sure the taxon is still in our list.
77
+ const taxon = taxa.getTaxon(name);
78
+ if (!taxon) {
79
+ // Don't process global exceptions if taxon is not in local list.
80
+ if (taxa.isSubset() && !v.local) {
81
+ continue;
82
+ }
83
+ errorLog.log(name, "has CCH exceptions but is not in taxa.tsv");
84
+ continue;
85
+ }
86
+
87
+ for (const [k] of Object.entries(exceptions)) {
88
+ const jepsonData = cchTaxa.get(name);
89
+ switch (k) {
90
+ case "notincch":
91
+ // Make sure it is really not in CCH data.
92
+ if (jepsonData) {
93
+ errorLog.log(
94
+ name,
95
+ "has CCH notincch exception but is in CCH data",
96
+ );
97
+ }
98
+ break;
99
+ default:
100
+ errorLog.log(name, "unrecognized CCH exception", k);
101
+ }
102
+ }
103
+ }
104
+ }
45
105
  }
46
106
 
47
107
  /**
48
108
  * @param {string} toolsDataPath
49
- * @param {import("../taxa.js").Taxa} taxa
109
+ * @param {import("../types.js").Taxa} taxa
50
110
  * @returns {Promise<CCHTaxa>}
51
111
  */
52
112
  async function getCCHTaxa(toolsDataPath, taxa) {
@@ -58,17 +118,22 @@ async function getCCHTaxa(toolsDataPath, taxa) {
58
118
  // Ignore ranks above species.
59
119
  return;
60
120
  }
61
- if (record.acceptance !== "1") {
62
- return;
63
- }
64
121
  if (!taxa.getTaxon(record.scientificName)) {
65
122
  // If we're not tracking the taxon, ignore it.
66
123
  return;
67
124
  }
125
+ if (record.acceptance !== "1" && data.has(record.scientificName)) {
126
+ // Only add the synonym if there is no main entry.
127
+ return;
128
+ }
68
129
  data.set(record.scientificName, { id: record.acceptedTaxonID });
69
130
  }
70
131
 
71
132
  const fileName = path.join(toolsDataPath, "taxa.csv");
133
+ if (!Files.exists(fileName)) {
134
+ await retrieveDataFile(toolsDataPath);
135
+ }
136
+
72
137
  const data = new Map();
73
138
 
74
139
  await CSV.parseFileStream(fileName, callback);
@@ -76,6 +141,41 @@ async function getCCHTaxa(toolsDataPath, taxa) {
76
141
  return data;
77
142
  }
78
143
 
144
+ /**
145
+ * @param {string} toolsDataPath
146
+ */
147
+ async function retrieveDataFile(toolsDataPath) {
148
+ const url =
149
+ "https://www.cch2.org/portal/taxa/taxonomy/taxonomydynamicdisplay.php";
150
+ console.log(`retrieving file from ${url}`);
151
+
152
+ const browser = await puppeteer.launch({ headless: true });
153
+
154
+ const page = await browser.newPage();
155
+
156
+ await page.goto(url);
157
+ await page.locator("#taxontarget").fill("Tracheophyta");
158
+
159
+ // See https://stackoverflow.com/questions/53471235/how-to-wait-for-all-downloads-to-complete-with-puppeteer
160
+ const session = await browser.target().createCDPSession();
161
+ await session.send("Browser.setDownloadBehavior", {
162
+ behavior: "allowAndName",
163
+ downloadPath: path.resolve(toolsDataPath),
164
+ eventsEnabled: true,
165
+ });
166
+
167
+ await page.locator('button[value="exportTaxonTree"]').click();
168
+
169
+ const filename = await waitUntilDownload(session);
170
+ // Download file name is the guid; rename it to taxa.csv.
171
+ renameSync(
172
+ path.join(toolsDataPath, filename),
173
+ path.join(toolsDataPath, "taxa.csv"),
174
+ );
175
+
176
+ await browser.close();
177
+ }
178
+
79
179
  /**
80
180
  * @param {string} dataDir
81
181
  * @param {Map<string,string>} idsToUpdate
@@ -93,3 +193,21 @@ function updateTaxaCSV(dataDir, idsToUpdate) {
93
193
 
94
194
  taxa.write();
95
195
  }
196
+
197
+ /**
198
+ * @param {import("puppeteer").CDPSession} session
199
+ * @returns {Promise<string>}
200
+ * @see https://stackoverflow.com/questions/53471235/how-to-wait-for-all-downloads-to-complete-with-puppeteer
201
+ * @see https://scrapeops.io/puppeteer-web-scraping-playbook/nodejs-puppeteer-downloading-a-file/#setting-a-custom-download-behaviour
202
+ */
203
+ async function waitUntilDownload(session) {
204
+ return new Promise((resolve, reject) => {
205
+ session.on("Browser.downloadProgress", (e) => {
206
+ if (e.state === "completed") {
207
+ resolve(e.guid);
208
+ } else if (e.state === "canceled") {
209
+ reject();
210
+ }
211
+ });
212
+ });
213
+ }