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

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/config.js CHANGED
@@ -2,9 +2,15 @@ import * as path from "node:path";
2
2
  import * as url from "node:url";
3
3
  import { Files } from "./files.js";
4
4
 
5
+ /** @type {Object<string,string>} */
6
+ const COUNTY_NAMES = {
7
+ ALA: "Alameda",
8
+ CCA: "Contra Costa",
9
+ };
10
+
5
11
  class Config {
6
12
  static #packageDir = path.dirname(
7
- path.dirname(url.fileURLToPath(import.meta.url))
13
+ path.dirname(url.fileURLToPath(import.meta.url)),
8
14
  );
9
15
 
10
16
  /** @type {Object<string,Object<string,Object<string,{}>>>} */
@@ -55,6 +61,14 @@ class Config {
55
61
  return [];
56
62
  }
57
63
 
64
+ /**
65
+ * @returns {string[]}
66
+ */
67
+ getCountyNames() {
68
+ const counties = this.getCountyCodes();
69
+ return counties.map((c) => COUNTY_NAMES[c]);
70
+ }
71
+
58
72
  /**
59
73
  * @param {string} name
60
74
  * @param {string} dflt
package/lib/csv.js CHANGED
@@ -13,7 +13,7 @@ class CSV {
13
13
  */
14
14
  static #getOptions(fileName, columns, delimiter) {
15
15
  /** @type {import("csv-parse").Options} */
16
- const options = { relax_column_count_less: true };
16
+ const options = { relax_column_count_less: true, bom: true };
17
17
  options.columns = columns;
18
18
  if (path.extname(fileName) === ".tsv") {
19
19
  options.delimiter = "\t";
@@ -44,6 +44,7 @@ class CSV {
44
44
  * @param {boolean|import("csv-parse").ColumnOption[]} columns
45
45
  * @param {string|undefined} delimiter
46
46
  * @param {function (any):void} callback
47
+ * @deprecated Use parseFileStream
47
48
  */
48
49
  static async parseStream(
49
50
  dir,
@@ -72,6 +73,27 @@ class CSV {
72
73
  await processFile();
73
74
  }
74
75
 
76
+ /**
77
+ * @template T
78
+ * @param {string} fileName
79
+ * @param {function (T):void} callback
80
+ */
81
+ static async parseFileStream(fileName, callback) {
82
+ const options = this.#getOptions(fileName, true, undefined);
83
+ const processFile = async () => {
84
+ const parser = fs.createReadStream(fileName).pipe(parse(options));
85
+ parser.on("readable", function () {
86
+ let record;
87
+ while ((record = parser.read()) !== null) {
88
+ callback(record);
89
+ }
90
+ });
91
+ await finished(parser);
92
+ };
93
+ // Parse the CSV content
94
+ await processFile();
95
+ }
96
+
75
97
  /**
76
98
  * @param {string} fileName
77
99
  * @param {boolean|import("csv-parse").ColumnOption[]|function (string[]):string[]} [columns]
@@ -5,7 +5,35 @@
5
5
  taxon_id?: string;
6
6
  }} InatObsOptions */
7
7
 
8
- class ExternalSites {
8
+ export class ExternalSites {
9
+ /**
10
+ * @param {import("./taxon.js").Taxon} taxon
11
+ * @param {import("./config.js").Config} config
12
+ * @returns {URL|undefined}
13
+ */
14
+ static getCCH2ObsLink(taxon, config) {
15
+ const url = new URL(
16
+ "https://www.cch2.org/portal/collections/listtabledisplay.php?usethes=1&taxontype=2&sortfield1=o.eventDate&sortorder=desc",
17
+ );
18
+ url.searchParams.set("county", config.getCountyNames().join(";"));
19
+ url.searchParams.set("taxa", taxon.getName());
20
+ return url;
21
+ }
22
+
23
+ /**
24
+ * @param {import("./taxon.js").Taxon} taxon
25
+ * @returns {URL|undefined}
26
+ */
27
+ static getCCH2RefLink(taxon) {
28
+ const id = taxon.getCCH2ID();
29
+ if (!id) {
30
+ return;
31
+ }
32
+ const url = new URL("https://www.cch2.org/portal/taxa/index.php");
33
+ url.searchParams.set("taxon", id);
34
+ return url;
35
+ }
36
+
9
37
  /**
10
38
  * @param {InatObsOptions} options
11
39
  */
@@ -44,5 +72,3 @@ class ExternalSites {
44
72
  return url.toString();
45
73
  }
46
74
  }
47
-
48
- export { ExternalSites };
package/lib/files.js CHANGED
@@ -140,7 +140,7 @@ class Files {
140
140
  if (entry.path === fileNameToUnzip) {
141
141
  await this.#createFileFromStream(
142
142
  targetFilePath,
143
- entry.stream()
143
+ entry.stream(),
144
144
  );
145
145
  break;
146
146
  }
package/lib/genera.js CHANGED
@@ -56,6 +56,9 @@ class Genus {
56
56
  return this.#data.familyObj;
57
57
  }
58
58
 
59
+ /**
60
+ * @returns {import("./taxon.js").Taxon[]}
61
+ */
59
62
  getTaxa() {
60
63
  return this.#data.taxa.sort((a, b) =>
61
64
  a.getName().localeCompare(b.getName()),
package/lib/htmltaxon.js CHANGED
@@ -54,6 +54,19 @@ const DEFAULT_TAXA_COLUMNS = [
54
54
  ];
55
55
 
56
56
  class HTMLTaxon {
57
+ /**
58
+ * @param {string[]} links
59
+ * @param {URL|string|undefined} href
60
+ * @param {string} label
61
+ */
62
+ static addLink(links, href, label) {
63
+ if (href === undefined) {
64
+ return;
65
+ }
66
+ const link = HTML.getLink(href.toString(), label, {}, true);
67
+ links.push(link);
68
+ }
69
+
57
70
  /**
58
71
  * @param {import("./taxon.js").Taxon} taxon
59
72
  * @returns {string|undefined}
package/lib/index.d.ts CHANGED
@@ -2,11 +2,14 @@ import { Command } from "commander";
2
2
 
3
3
  // Types
4
4
 
5
+ export type NativeStatusCode = "N" | "NC" | "U" | "X";
6
+
5
7
  export type TaxonData = {
6
8
  bloom_end: string;
7
9
  bloom_start: string;
8
10
  calrecnum: string;
9
11
  calscape_cn?: string;
12
+ cch2_id: string;
10
13
  CESA: string;
11
14
  "common name": string;
12
15
  CRPR: string;
@@ -18,7 +21,7 @@ export type TaxonData = {
18
21
  life_cycle: string;
19
22
  "RPI ID": string;
20
23
  SRank: string;
21
- status: "N" | "NC" | "U" | "X";
24
+ status: NativeStatusCode;
22
25
  taxon_name: string;
23
26
  };
24
27
 
@@ -58,6 +61,15 @@ export class Exceptions {
58
61
  hasException(name: string, cat: string, subcat: string): boolean;
59
62
  }
60
63
 
64
+ export class ExternalSites {
65
+ static getCCH2ObsLink(taxon: Taxon, config: Config): URL | undefined;
66
+ static getCCH2RefLink(taxon: Taxon): URL | undefined;
67
+ }
68
+
69
+ export class Family {
70
+ getName(): string;
71
+ }
72
+
61
73
  export class Files {
62
74
  static exists(fileName: string): boolean;
63
75
  static fetch(
@@ -69,6 +81,12 @@ export class Files {
69
81
  static write(fileName: string, data: string, overwrite: boolean): void;
70
82
  }
71
83
 
84
+ export class Genera {}
85
+
86
+ export class Genus {
87
+ getTaxa(): Taxon[];
88
+ }
89
+
72
90
  export class HTML {
73
91
  static arrayToLI(items: string[]): string;
74
92
  static getLink(
@@ -90,6 +108,11 @@ export class HTML {
90
108
  }
91
109
 
92
110
  export class HTMLTaxon {
111
+ static addLink(
112
+ links: string[],
113
+ href: URL | string | undefined,
114
+ label: string,
115
+ ): void;
93
116
  static getFooterHTML(taxon: Taxon): string;
94
117
  static getListSectionHTML(
95
118
  list: string[],
@@ -118,12 +141,6 @@ export class Program {
118
141
  static getProgram(): Command;
119
142
  }
120
143
 
121
- export class Family {}
122
-
123
- export class Genera {}
124
-
125
- export class Genus {}
126
-
127
144
  export class Taxa {
128
145
  constructor(
129
146
  inclusionList: Record<string, TaxonData> | true,
@@ -145,6 +162,7 @@ export class Taxon {
145
162
  getCNDDBRank(): string | undefined;
146
163
  getCommonNames(): string[];
147
164
  getFamily(): Family;
165
+ getFileName(): string;
148
166
  getFESA(): string | undefined;
149
167
  getGenus(): Genus;
150
168
  getGenusName(): string;
package/lib/index.js CHANGED
@@ -3,6 +3,7 @@ import { Config } from "./config.js";
3
3
  import { CSV } from "./csv.js";
4
4
  import { ErrorLog } from "./errorlog.js";
5
5
  import { Exceptions } from "./exceptions.js";
6
+ import { ExternalSites } from "./externalsites.js";
6
7
  import { Families } from "./families.js";
7
8
  import { Files } from "./files.js";
8
9
  import { HTML } from "./html.js";
@@ -11,7 +12,7 @@ import { Jekyll } from "./jekyll.js";
11
12
  import { PlantBook } from "./ebook/plantbook.js";
12
13
  import { Program } from "./program.js";
13
14
  import { Taxa } from "./taxa.js";
14
- import { Taxon, TAXA_COLNAMES } from "./taxon.js";
15
+ import { Taxon } from "./taxon.js";
15
16
 
16
17
  export {
17
18
  BasePageRenderer,
@@ -19,6 +20,7 @@ export {
19
20
  CSV,
20
21
  ErrorLog,
21
22
  Exceptions,
23
+ ExternalSites,
22
24
  Families,
23
25
  Files,
24
26
  HTML,
@@ -27,6 +29,5 @@ export {
27
29
  PlantBook,
28
30
  Program,
29
31
  Taxa,
30
- TAXA_COLNAMES,
31
32
  Taxon,
32
33
  };
package/lib/taxa.js CHANGED
@@ -255,7 +255,7 @@ class Taxa {
255
255
  for (const row of taxaCSV) {
256
256
  const name = row["taxon_name"];
257
257
 
258
- /** @type {import("./index.js").TaxonData|{status?:import("./taxon.js").StatusCode}} */
258
+ /** @type {import("./index.js").TaxonData|{status?:import("./index.js").NativeStatusCode}} */
259
259
  let taxon_overrides = {};
260
260
  if (inclusionList !== true) {
261
261
  taxon_overrides = inclusionList[name];
package/lib/taxon.js CHANGED
@@ -1,17 +1,6 @@
1
1
  import { HTML } from "./html.js";
2
2
  import { RarePlants } from "./rareplants.js";
3
3
 
4
- /**
5
- * @typedef {"N" | "NC" | "U" | "X"} StatusCode
6
- */
7
-
8
- const TAXA_COLNAMES = {
9
- BLOOM_START: "bloom_start",
10
- BLOOM_END: "bloom_end",
11
- COMMON_NAME: "common name",
12
- FLOWER_COLOR: "flower_color",
13
- };
14
-
15
4
  class Taxon {
16
5
  #genera;
17
6
  #name;
@@ -25,6 +14,7 @@ class Taxon {
25
14
  #iNatID;
26
15
  /**@type {string|undefined} */
27
16
  #iNatSyn;
17
+ #cch2id;
28
18
  #calscapeCN;
29
19
  #lifeCycle;
30
20
  #flowerColors;
@@ -62,6 +52,7 @@ class Taxon {
62
52
  this.#jepsonID = data["jepson id"];
63
53
  this.#calRecNum = data["calrecnum"];
64
54
  this.#iNatID = data["inat id"];
55
+ this.#cch2id = data.cch2_id;
65
56
  this.#calscapeCN =
66
57
  data.calscape_cn === "" ? undefined : data.calscape_cn;
67
58
  this.#lifeCycle = data.life_cycle;
@@ -173,6 +164,13 @@ class Taxon {
173
164
  return name.replace(" subsp.", " ssp.");
174
165
  }
175
166
 
167
+ /**
168
+ * @returns {string}
169
+ */
170
+ getCCH2ID() {
171
+ return this.#cch2id;
172
+ }
173
+
176
174
  getCESA() {
177
175
  return this.#cesa;
178
176
  }
@@ -303,12 +301,18 @@ class Taxon {
303
301
  return this.#rankRPI;
304
302
  }
305
303
 
304
+ /**
305
+ * @deprecated
306
+ */
306
307
  getRPIRankAndThreatTooltip() {
307
308
  return RarePlants.getRPIRankAndThreatDescriptions(
308
309
  this.getRPIRankAndThreat(),
309
310
  ).join("<br>");
310
311
  }
311
312
 
313
+ /**
314
+ * @deprecated
315
+ */
312
316
  getRPITaxonLink() {
313
317
  const rpiID = this.getRPIID();
314
318
  if (!rpiID) {
@@ -323,9 +327,6 @@ class Taxon {
323
327
  return link;
324
328
  }
325
329
 
326
- /**
327
- * @returns {StatusCode}
328
- */
329
330
  getStatus() {
330
331
  return this.#status;
331
332
  }
@@ -385,4 +386,4 @@ class Taxon {
385
386
  }
386
387
  }
387
388
 
388
- export { TAXA_COLNAMES, Taxon };
389
+ export { Taxon };
@@ -23,7 +23,6 @@ export class Calflora {
23
23
  static #taxa = {};
24
24
 
25
25
  /**
26
- *
27
26
  * @param {string} toolsDataDir
28
27
  * @param {string} dataDir
29
28
  * @param {import("../taxa.js").Taxa} taxa
@@ -0,0 +1,95 @@
1
+ import path from "node:path";
2
+ import { CSV } from "../csv.js";
3
+ import { TaxaCSV } from "./taxacsv.js";
4
+
5
+ /**
6
+ * @typedef {{id:string}} CCHTaxon
7
+ * @typedef {Map<string,CCHTaxon>} CCHTaxa
8
+ */
9
+
10
+ export class CCH2 {
11
+ /**
12
+ * @param {string} toolsDataDir
13
+ * @param {string} dataDir
14
+ * @param {import("../taxa.js").Taxa} taxa
15
+ * @param {import("../errorlog.js").ErrorLog} errorLog
16
+ * @param {boolean} update
17
+ */
18
+ static async analyze(toolsDataDir, dataDir, taxa, errorLog, update) {
19
+ const toolsDataPath = path.join(toolsDataDir, "cch2");
20
+
21
+ const cchTaxa = await getCCHTaxa(toolsDataPath, taxa);
22
+
23
+ const idsToUpdate = new Map();
24
+ for (const taxon of taxa.getTaxonList()) {
25
+ const cchTaxon = cchTaxa.get(taxon.getName());
26
+ if (!cchTaxon) {
27
+ errorLog.log(taxon.getName(), "not found in CCH data");
28
+ continue;
29
+ }
30
+ if (cchTaxon.id !== taxon.getCCH2ID()) {
31
+ errorLog.log(
32
+ taxon.getName(),
33
+ "id in CCH data does not match id in taxa.csv",
34
+ cchTaxon.id,
35
+ taxon.getCCH2ID(),
36
+ );
37
+ idsToUpdate.set(taxon.getName(), cchTaxon.id);
38
+ }
39
+ }
40
+
41
+ if (update) {
42
+ updateTaxaCSV(dataDir, idsToUpdate);
43
+ }
44
+ }
45
+ }
46
+
47
+ /**
48
+ * @param {string} toolsDataPath
49
+ * @param {import("../taxa.js").Taxa} taxa
50
+ * @returns {Promise<CCHTaxa>}
51
+ */
52
+ async function getCCHTaxa(toolsDataPath, taxa) {
53
+ /**
54
+ * @param {{taxonID:string,scientificName:string,rankID:string,acceptance:"0"|"1",acceptedTaxonID:string}} record
55
+ */
56
+ function callback(record) {
57
+ if (parseInt(record.rankID) < 220) {
58
+ // Ignore ranks above species.
59
+ return;
60
+ }
61
+ if (record.acceptance !== "1") {
62
+ return;
63
+ }
64
+ if (!taxa.getTaxon(record.scientificName)) {
65
+ // If we're not tracking the taxon, ignore it.
66
+ return;
67
+ }
68
+ data.set(record.scientificName, { id: record.acceptedTaxonID });
69
+ }
70
+
71
+ const fileName = path.join(toolsDataPath, "taxa.csv");
72
+ const data = new Map();
73
+
74
+ await CSV.parseFileStream(fileName, callback);
75
+
76
+ return data;
77
+ }
78
+
79
+ /**
80
+ * @param {string} dataDir
81
+ * @param {Map<string,string>} idsToUpdate
82
+ */
83
+ function updateTaxaCSV(dataDir, idsToUpdate) {
84
+ const taxa = new TaxaCSV(dataDir);
85
+
86
+ for (const taxonData of taxa.getTaxa()) {
87
+ const id = idsToUpdate.get(taxonData.taxon_name);
88
+ if (!id) {
89
+ continue;
90
+ }
91
+ taxonData.cch2_id = id;
92
+ }
93
+
94
+ taxa.write();
95
+ }
@@ -228,7 +228,7 @@ export class JepsonEFlora {
228
228
 
229
229
  /**
230
230
  * @param {JepsonTaxon} jepsInfo
231
- * @returns {import("../taxon.js").StatusCode|undefined}
231
+ * @returns {import("../index.js").NativeStatusCode|undefined}
232
232
  */
233
233
  #getStatusCode(jepsInfo) {
234
234
  switch (jepsInfo.type) {
@@ -5,7 +5,7 @@ import { ExternalSites } from "../externalsites.js";
5
5
  import { HTML } from "../html.js";
6
6
  import { HTMLTaxon } from "../htmltaxon.js";
7
7
 
8
- class PageTaxon extends GenericPage {
8
+ export class PageTaxon extends GenericPage {
9
9
  #config;
10
10
  #taxon;
11
11
 
@@ -42,6 +42,11 @@ class PageTaxon extends GenericPage {
42
42
  if (rpiLink) {
43
43
  links.push(rpiLink);
44
44
  }
45
+ HTMLTaxon.addLink(
46
+ links,
47
+ ExternalSites.getCCH2RefLink(this.#taxon),
48
+ "CCH2",
49
+ );
45
50
  return links;
46
51
  }
47
52
 
@@ -76,6 +81,11 @@ class PageTaxon extends GenericPage {
76
81
  ),
77
82
  );
78
83
  }
84
+ HTMLTaxon.addLink(
85
+ links,
86
+ ExternalSites.getCCH2ObsLink(this.#taxon, this.#config),
87
+ "CCH2",
88
+ );
79
89
 
80
90
  return links;
81
91
  }
@@ -218,5 +228,3 @@ class PageTaxon extends GenericPage {
218
228
  this.writeFile(html);
219
229
  }
220
230
  }
221
-
222
- export { PageTaxon };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ca-plant-list/ca-plant-list",
3
- "version": "0.4.20",
3
+ "version": "0.4.21",
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": {
@@ -13,10 +13,12 @@ import { Config } from "../lib/config.js";
13
13
  import { Taxa } from "../lib/taxa.js";
14
14
  import { SupplementalText } from "../lib/tools/supplementaltext.js";
15
15
  import { JepsonFamilies } from "../lib/tools/jepsonfamilies.js";
16
+ import { CCH2 } from "../lib/tools/cch2.js";
16
17
 
17
18
  const TOOLS = {
18
19
  CALFLORA: "calflora",
19
20
  CALSCAPE: "calscape",
21
+ CCH2: "cch",
20
22
  INAT: "inat",
21
23
  JEPSON_EFLORA: "jepson-eflora",
22
24
  JEPSON_FAM: "jepson-families",
@@ -27,6 +29,7 @@ const TOOLS = {
27
29
  const ALL_TOOLS = [
28
30
  TOOLS.CALFLORA,
29
31
  TOOLS.CALSCAPE,
32
+ TOOLS.CCH2,
30
33
  TOOLS.INAT,
31
34
  TOOLS.JEPSON_EFLORA,
32
35
  TOOLS.RPI,
@@ -77,6 +80,15 @@ async function build(program, options) {
77
80
  !!options.update,
78
81
  );
79
82
  break;
83
+ case TOOLS.CCH2:
84
+ await CCH2.analyze(
85
+ TOOLS_DATA_DIR,
86
+ options.datadir,
87
+ taxa,
88
+ errorLog,
89
+ !!options.update,
90
+ );
91
+ break;
80
92
  case TOOLS.INAT:
81
93
  await INat.analyze(
82
94
  TOOLS_DATA_DIR,