@ca-plant-list/ca-plant-list 0.4.10 → 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.
Files changed (39) hide show
  1. package/data/exceptions.json +16 -1
  2. package/data/inattaxonphotos.csv +27 -0
  3. package/data/synonyms.csv +7 -0
  4. package/data/taxa.csv +1364 -1361
  5. package/data/text/Asclepias-californica.md +1 -0
  6. package/data/text/Asclepias-cordifolia.md +1 -0
  7. package/data/text/Asclepias-eriocarpa.md +1 -0
  8. package/data/text/Asclepias-speciosa.md +1 -0
  9. package/data/text/Calystegia-malacophylla-subsp-pedicellata.md +1 -0
  10. package/data/text/Calystegia-purpurata-subsp-purpurata.md +1 -0
  11. package/data/text/Calystegia-sepium-subsp-limnophila.md +1 -0
  12. package/data/text/Calystegia-silvatica-subsp-disjuncta.md +1 -0
  13. package/data/text/Ribes-aureum-var-gracillimum.md +1 -0
  14. package/data/text/Ribes-californicum-var-californicum.md +1 -0
  15. package/data/text/Ribes-divaricatum-var-pubiflorum.md +1 -0
  16. package/data/text/Ribes-malvaceum-var-malvaceum.md +1 -0
  17. package/data/text/Ribes-menziesii-var-menziesii.md +1 -0
  18. package/data/text/Ribes-quercetorum.md +1 -0
  19. package/data/text/Ribes-sanguineum-var-glutinosum.md +1 -0
  20. package/data/text/Ribes-speciosum.md +1 -0
  21. package/data/text/Toxicoscordion-fremontii.md +1 -1
  22. package/data/text/Toxicoscordion-paniculatum.md +1 -1
  23. package/data/text/Toxicoscordion-venenosum-var-venenosum.md +1 -1
  24. package/data/text/Viola-purpurea-subsp-purpurea.md +1 -0
  25. package/data/text/Viola-purpurea-subsp-quercetorum.md +1 -1
  26. package/lib/csv.js +37 -1
  27. package/lib/exceptions.js +2 -2
  28. package/lib/htmltaxon.js +17 -0
  29. package/lib/taxa.js +8 -2
  30. package/lib/taxon.js +24 -6
  31. package/lib/tools/calflora.js +220 -0
  32. package/lib/tools/calscape.js +214 -0
  33. package/lib/tools/taxacsv.js +31 -0
  34. package/lib/web/pagetaxon.js +5 -1
  35. package/package.json +4 -3
  36. package/schemas/exceptions.schema.json +6 -1
  37. package/scripts/cpl-photos.js +5 -1
  38. package/scripts/cpl-tools.js +177 -0
  39. package/types/classes.d.ts +6 -0
@@ -0,0 +1 @@
1
+ Plant hairy. Anther heads protruding beyond hoods. Petioles short or non-existent, but leaves not clasping stem. Horn small if present.
@@ -0,0 +1 @@
1
+ Plant not hairy. Anther heads about the same height as hoods. No horn in flower.
@@ -0,0 +1 @@
1
+ Plant wooly. Horns and anther heads at about same level as hoods.
@@ -0,0 +1 @@
1
+ Plant somewhat hairy. Hoods protrude beyond anther head. Hoods ascending, with pointed tips.
@@ -0,0 +1 @@
1
+ Bracts at least 4mm wide, just below [calyx](./g/calyx.html), mostly hiding calyx. Leaves and bracts hairy. Usually not climbing.
@@ -0,0 +1 @@
1
+ Bracts usually not hiding [calyx](./g/calyx.html). Not hairy. Leaves usually with a V-shaped notch and pointy lobes (_C. occidentalis_ generally with rounded notch and lobes). Generally climbing.
@@ -0,0 +1 @@
1
+ Bracts at least 4mm wide, just below [calyx](./g/calyx.html), mostly hiding calyx. Bracts not sac-like at base. Bracts not stronly overlapping. Corolla 30-60 mm (usually smaller than _C. silvatica_). Leaves 4-8 cm (generally smaller than _C. silvatica_).
@@ -0,0 +1 @@
1
+ Bracts at least 4mm wide, just below [calyx](./g/calyx.html), mostly hiding calyx. Bracts sac-like at base. Bracts strongly overlapping. Corolla 55-75 mm (usually larger than _C. sepium_). Leaves 5-12 cm (generally larger than _C. sepium_).
@@ -0,0 +1 @@
1
+ No spines on stem. Petals turning red with age.
@@ -0,0 +1 @@
1
+ Stems with spines. Anthers extending beyond petals. [Styles](./g/style.html) not hairy at base. Lower side of leaf not glandular. Sepals green- or red-tinged.
@@ -0,0 +1 @@
1
+ Stems with spines. Anthers extending beyond petals at least 3 mm. [Styles](./g/style.html) hairy at base. Fruit black, not hairy.
@@ -0,0 +1 @@
1
+ No spines on stem. [Styles](./g/style.html) hairy at base.
@@ -0,0 +1 @@
1
+ Stems with spines. Anthers extending beyond petals. [Styles](./g/style.html) not hairy at base. Lower side of leaf glandular. Sepals purple.
@@ -0,0 +1 @@
1
+ Stems with spines. Anthers not extending past petals.
@@ -0,0 +1 @@
1
+ No spines on stem. [Styles](./g/style.html) not hairy at base.
@@ -0,0 +1 @@
1
+ 4 sepals.
@@ -1 +1 @@
1
- Leaves 8-30 mm wide. [Perianth](./perianth.html) parts 5-15 mm long.
1
+ Leaves 8-30 mm wide. [Perianth](./g/perianth.html) parts 5-15 mm long.
@@ -1 +1 @@
1
- Leaves 6-16 mm wide. [Perianth](./perianth.html) parts 2-6 mm long. Perianth parts unequal, outer parts generally without claws, inner parts with claws. Stamens at least as long as perianth parts.
1
+ Leaves 6-16 mm wide. [Perianth](./g/perianth.html) parts 2-6 mm long. Perianth parts unequal, outer parts generally without claws, inner parts with claws. Stamens at least as long as perianth parts.
@@ -1 +1 @@
1
- Leaves 4-10 mm wide. [Perianth](./perianth.html) parts 4-6 mm long. Stamens at least as long as perianth parts.
1
+ Leaves 4-10 mm wide. [Perianth](./g/perianth.html) parts 4-6 mm long. Stamens at least as long as perianth parts.
@@ -0,0 +1 @@
1
+ Leaves not compound, usually tinted purple on lower side.
@@ -1 +1 @@
1
- Leaves not compound.
1
+ Leaves not compound, usually not tinted purple on lower side.
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 writeFile(fileName, data, headerData) {
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 = CSV.parseFile(dataDir, "taxa.csv");
66
- this.#loadTaxa(taxaCSV, inclusionList, taxonFactory, showFlowerErrors);
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( [photo] );
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 };