@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,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
+ }
@@ -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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ca-plant-list/ca-plant-list",
3
- "version": "0.4.10",
3
+ "version": "0.4.12",
4
4
  "description": "Tools to create Jekyll files for a website listing plants in an area of California.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -16,7 +16,8 @@
16
16
  "bin": {
17
17
  "ca-plant-list": "scripts/build-site.js",
18
18
  "ca-plant-book": "scripts/build-ebook.js",
19
- "cpl-photos": "scripts/cpl-photos.js"
19
+ "cpl-photos": "scripts/cpl-photos.js",
20
+ "cpl-tools": "scripts/cpl-tools.js"
20
21
  },
21
22
  "dependencies": {
22
23
  "archiver": "^5.3.1",
@@ -36,8 +37,8 @@
36
37
  "@types/markdown-it": "^14.1.2",
37
38
  "@types/node": "^22.7.8",
38
39
  "@types/unzipper": "^0.10.9",
39
- "ajv-cli": "^5.0.0",
40
40
  "eslint": "^9.13.0",
41
+ "exceljs": "^4.4.0",
41
42
  "prettier": "^3.3.3",
42
43
  "typescript": "^5.6.3"
43
44
  }
@@ -9,12 +9,17 @@
9
9
  "badjepsonid": {
10
10
  "const": true
11
11
  },
12
+ "native": { "type": "boolean" },
12
13
  "notintaxondata": {
13
14
  "const": true
14
15
  }
15
16
  },
16
17
  "additionalProperties": false
17
18
  },
19
+ "calscape": {
20
+ "type": "object",
21
+ "properties": { "notnative": { "const": true } }
22
+ },
18
23
  "comment": {
19
24
  "type": "string"
20
25
  },
@@ -54,4 +59,4 @@
54
59
  },
55
60
  "additionalProperties": false
56
61
  }
57
- }
62
+ }
@@ -74,7 +74,11 @@ async function addMissingPhotos(options) {
74
74
  }
75
75
  }
76
76
 
77
- CSV.writeFile(`${options.outputdir}/${PHOTO_FILE_NAME}`, data, headers);
77
+ CSV.writeFileArray(
78
+ `${options.outputdir}/${PHOTO_FILE_NAME}`,
79
+ data,
80
+ headers,
81
+ );
78
82
  }
79
83
 
80
84
  /**
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+
3
+ import * as path from "node:path";
4
+ import { Option } from "commander";
5
+ import { Taxa } from "../lib/taxa.js";
6
+ import { Program } from "../lib/program.js";
7
+ import { Calflora } from "../lib/tools/calflora.js";
8
+ import { Exceptions } from "../lib/exceptions.js";
9
+ import { ErrorLog } from "../lib/errorlog.js";
10
+ import { Calscape } from "../lib/tools/calscape.js";
11
+
12
+ const TOOLS = {
13
+ CALFLORA: "calflora",
14
+ CALSCAPE: "calscape",
15
+ INAT: "inat",
16
+ JEPSON_EFLORA: "jepson-eflora",
17
+ JEPSON_FAM: "jepson-families",
18
+ RPI: "rpi",
19
+ TEXT: "text",
20
+ };
21
+
22
+ const ALL_TOOLS = [
23
+ TOOLS.CALFLORA,
24
+ TOOLS.CALSCAPE,
25
+ TOOLS.INAT,
26
+ TOOLS.JEPSON_EFLORA,
27
+ TOOLS.RPI,
28
+ TOOLS.TEXT,
29
+ ];
30
+
31
+ const OPT_LOADER = "loader";
32
+ const OPT_TOOL = "tool";
33
+
34
+ const TOOLS_DATA_DIR = "./external_data";
35
+
36
+ /**
37
+ * @param {import("commander").Command} program
38
+ * @param {import("commander").OptionValues} options
39
+ */
40
+ async function build(program, options) {
41
+ let tools = options[OPT_TOOL];
42
+ if (!tools) {
43
+ program.help();
44
+ }
45
+ if (tools[0] === "all") {
46
+ tools = ALL_TOOLS;
47
+ }
48
+
49
+ const exceptions = new Exceptions(options.datadir);
50
+ // const config = new Config(options.datadir);
51
+ const taxa = await getTaxa(options);
52
+
53
+ const errorLog = new ErrorLog(options.outputdir + "/log.tsv", true);
54
+ for (const tool of tools) {
55
+ switch (tool) {
56
+ case TOOLS.CALFLORA:
57
+ await Calflora.analyze(
58
+ TOOLS_DATA_DIR,
59
+ taxa,
60
+ exceptions,
61
+ errorLog,
62
+ );
63
+ break;
64
+ case TOOLS.CALSCAPE:
65
+ await Calscape.analyze(
66
+ TOOLS_DATA_DIR,
67
+ options.datadir,
68
+ taxa,
69
+ exceptions,
70
+ errorLog,
71
+ !!options.update,
72
+ );
73
+ break;
74
+ case TOOLS.INAT:
75
+ // await INat.analyze(
76
+ // TOOLS_DATA_DIR,
77
+ // taxa,
78
+ // exceptions,
79
+ // errorLog,
80
+ // options.inTaxafile,
81
+ // );
82
+ break;
83
+ case TOOLS.JEPSON_EFLORA: {
84
+ // const eflora = new JepsonEFlora(
85
+ // TOOLS_DATA_DIR,
86
+ // taxa,
87
+ // errorLog,
88
+ // options.efLognotes,
89
+ // );
90
+ // await eflora.analyze(exceptions);
91
+ break;
92
+ }
93
+ case TOOLS.JEPSON_FAM:
94
+ // await JepsonFamilies.build(TOOLS_DATA_DIR, options.outputdir);
95
+ break;
96
+ case TOOLS.RPI:
97
+ // await RPI.analyze(
98
+ // TOOLS_DATA_DIR,
99
+ // taxa,
100
+ // config,
101
+ // exceptions,
102
+ // errorLog,
103
+ // );
104
+ break;
105
+ case TOOLS.TEXT:
106
+ // SupplementalText.analyze(taxa, errorLog);
107
+ break;
108
+ default:
109
+ console.log("unrecognized tool: " + tool);
110
+ return;
111
+ }
112
+ }
113
+
114
+ errorLog.write();
115
+ }
116
+
117
+ /**
118
+ * @param {import("commander").OptionValues} options
119
+ */
120
+ async function getTaxa(options) {
121
+ const errorLog = new ErrorLog(options.outputdir + "/errors.tsv", true);
122
+
123
+ const loader = options[OPT_LOADER];
124
+ let taxa;
125
+ if (loader) {
126
+ const taxaLoaderClass = await import("file:" + path.resolve(loader));
127
+ taxa = await taxaLoaderClass.TaxaLoader.loadTaxa(options, errorLog);
128
+ } else {
129
+ taxa = new Taxa(
130
+ Program.getIncludeList(options.datadir),
131
+ errorLog,
132
+ options.showFlowerErrors,
133
+ );
134
+ }
135
+
136
+ errorLog.write();
137
+ return taxa;
138
+ }
139
+
140
+ const program = Program.getProgram();
141
+ program.addOption(
142
+ new Option(
143
+ "-t, --tool <tool...>",
144
+ "The tools to run. Value may be any subset of the tools below.",
145
+ ).choices(["all"].concat(ALL_TOOLS).concat(TOOLS.JEPSON_FAM)),
146
+ );
147
+ program.option(
148
+ "--in-taxafile <file>",
149
+ "The name of the file containing the iNaturalist taxa. Can be used for testing on a smaller subset of the iNaturalist data.",
150
+ "inat_taxa.csv",
151
+ );
152
+ program.option(
153
+ "--ef-lognotes",
154
+ "When running the jepson-eflora tool, include eFlora notes, invalid names, etc. in the log file.",
155
+ );
156
+ program.option(
157
+ "--loader <path>",
158
+ "The path (relative to the current directory) of the JavaScript file containing the TaxaLoader class. If not provided, the default TaxaLoader will be used.",
159
+ );
160
+ program.option("--update", "Update taxa.csv to remove errors if possible.");
161
+ program.addHelpText(
162
+ "after",
163
+ `
164
+ Tools:
165
+ 'all' runs the 'calflora', 'inat', 'jepson-eflora', 'rpi', and 'text' tools.
166
+ '${TOOLS.CALFLORA}' retrieves data from Calflora and compares with local data.
167
+ '${TOOLS.CALSCAPE}' retrieves data from Calscape and compares with local data.
168
+ '${TOOLS.INAT}' retrieves data from iNaturalist and compares with local data.
169
+ '${TOOLS.JEPSON_EFLORA}' retrieves data from Jepson eFlora indexes and compares with local data.
170
+ '${TOOLS.JEPSON_FAM}' retrieves section, family and genus data from Jepson eFlora and creates data files for use by ca-plant-list.
171
+ '${TOOLS.RPI}' retrieves data from the CNPS Rare Plant Inventory and compares with local data.
172
+ '${TOOLS.TEXT}' checks supplemental text files to make sure their names are referenced.
173
+ `,
174
+ );
175
+ program.action((options) => build(program, options));
176
+
177
+ await program.parseAsync();
@@ -83,6 +83,7 @@ declare class Taxa {
83
83
  getFlowerColors(): FlowerColor[];
84
84
  getTaxon(name: string): Taxon;
85
85
  getTaxonList(): Taxon[];
86
+ isSubset(): boolean;
86
87
  }
87
88
 
88
89
  declare class TaxaCol {
@@ -96,8 +97,11 @@ declare class Taxon {
96
97
  getBaseFileName(): string;
97
98
  getBloomEnd(): number | undefined;
98
99
  getBloomStart(): number | undefined;
100
+ getCalfloraID(): string;
99
101
  getCalfloraName(): string;
100
102
  getCalfloraTaxonLink(): string | undefined;
103
+ getCalscapeCommonName(): string | undefined;
104
+ getCalscapeName(): string;
101
105
  getCESA(): string | undefined;
102
106
  getCommonNames(): string[];
103
107
  getFamily(): Family;
@@ -123,6 +127,7 @@ declare class Taxon {
123
127
  getRPITaxonLink(): string;
124
128
  getStatusDescription(config: Config): string;
125
129
  getSynonyms(): string[];
130
+ isCANative(): boolean;
126
131
  isNative(): boolean;
127
132
  }
128
133
 
@@ -130,6 +135,7 @@ declare class TaxonData {
130
135
  bloom_end: string;
131
136
  bloom_start: string;
132
137
  calrecnum: string;
138
+ calscape_cn?: string;
133
139
  CESA: string;
134
140
  "common name": string;
135
141
  CRPR: string;