@ca-plant-list/ca-plant-list 0.4.33 → 0.4.36

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 (79) hide show
  1. package/data/glossary/peduncle.md +1 -1
  2. package/data/inatobsphotos.csv +499 -75
  3. package/data/inattaxonphotos.csv +728 -242
  4. package/data/synonyms.csv +86 -0
  5. package/data/taxa.csv +1985 -1905
  6. package/data/text/Allium-obtusum-var-obtusum.md +1 -0
  7. package/data/text/Allium-validum.md +1 -0
  8. package/data/text/Amelanchier-alnifolia-var-pumila.md +1 -0
  9. package/data/text/Amelanchier-utahensis.md +1 -0
  10. package/data/text/Angelica-breweri.md +1 -0
  11. package/data/text/Angelica-capitellata.md +1 -0
  12. package/data/text/Castilleja-applegatei-subsp-pallida.md +1 -0
  13. package/data/text/Castilleja-miniata-subsp-miniata.md +1 -0
  14. package/data/text/Ceanothus-cordulatus.md +1 -0
  15. package/data/text/Ceanothus-velutinus.md +1 -0
  16. package/data/text/Collinsia-parviflora.md +1 -0
  17. package/data/text/Collinsia-torreyi.md +1 -0
  18. package/data/text/Ericameria-nauseosa-var-speciosa.md +1 -0
  19. package/data/text/Ericameria-nauseosa.md +1 -0
  20. package/data/text/Erigeron-algidus.md +1 -0
  21. package/data/text/Erigeron-coulteri.md +1 -0
  22. package/data/text/Hackelia-micrantha.md +1 -0
  23. package/data/text/Hieracium-albiflorum.md +1 -0
  24. package/data/text/Hieracium-horridum.md +1 -0
  25. package/data/text/Hieracium-triste.md +1 -0
  26. package/data/text/Linanthus-pungens-subsp-pulchriflorus.md +1 -0
  27. package/data/text/Lupinus-argenteus-var-meionanthus.md +1 -0
  28. package/data/text/Lupinus-latifolius-var-latifolius.md +1 -1
  29. package/data/text/Lupinus-lepidus-var-lobbii.md +1 -0
  30. package/data/text/Lupinus-polyphyllus-var-burkei.md +1 -0
  31. package/data/text/Pedicularis-attollens.md +1 -0
  32. package/data/text/Pedicularis-groenlandica.md +1 -0
  33. package/data/text/Penstemon-deustus-var-deustus.md +1 -0
  34. package/data/text/Penstemon-deustus-var-pedicellatus.md +1 -0
  35. package/data/text/Penstemon-heterodoxus-var-heterodoxus.md +1 -0
  36. package/data/text/Penstemon-rydbergii-var-oreocharis.md +1 -0
  37. package/data/text/Phlox-diffusa.md +1 -0
  38. package/data/text/Potentilla-flabellifolia.md +1 -0
  39. package/data/text/Potentilla-gracilis-var-fastigiata.md +1 -0
  40. package/data/text/Primula-jeffreyi.md +1 -1
  41. package/data/text/Primula-tetrandra.md +1 -1
  42. package/data/text/Pseudognaphalium-beneolens.md +1 -0
  43. package/data/text/Pseudognaphalium-californicum.md +1 -0
  44. package/data/text/Pseudognaphalium-ramosissimum.md +1 -0
  45. package/data/text/Pyrola-asarifolia-subsp-asarifolia.md +1 -0
  46. package/data/text/Pyrola-dentata.md +1 -0
  47. package/data/text/Pyrola-picta.md +1 -0
  48. package/data/text/Salix-lemmonii.md +1 -0
  49. package/data/text/Salix-scouleriana.md +1 -0
  50. package/data/text/Senecio-integerrimus-var-exaltatus.md +1 -0
  51. package/data/text/Senecio-integerrimus-var-major.md +1 -0
  52. package/data/text/Senecio-triangularis.md +1 -0
  53. package/data/text/Sorbus-californica.md +1 -0
  54. package/data/text/Sorbus-scopulina.md +1 -0
  55. package/ebook/css/main.css +20 -13
  56. package/lib/csv.js +0 -2
  57. package/lib/ebook/ebook.js +178 -178
  58. package/lib/ebook/pages/taxonpage.js +19 -9
  59. package/lib/ebook/plantbook.js +17 -3
  60. package/lib/externalsites.js +12 -0
  61. package/lib/htmltaxon.js +13 -2
  62. package/lib/index.d.ts +3 -0
  63. package/lib/markdown.js +1 -1
  64. package/lib/program.js +0 -26
  65. package/lib/sitegenerator.js +4 -4
  66. package/lib/taxonomy/taxa.js +26 -5
  67. package/lib/taxonomy/taxon.js +12 -2
  68. package/lib/tools/calflora.js +0 -2
  69. package/lib/tools/calipc.js +111 -0
  70. package/lib/tools/taxacsv.js +5 -4
  71. package/lib/utils/eleventyGenerator.js +2 -0
  72. package/lib/utils/inat-tools.js +8 -4
  73. package/lib/web/pageTaxon.js +1 -0
  74. package/package.json +6 -6
  75. package/scripts/build-ebook.js +21 -6
  76. package/scripts/build-site.js +1 -1
  77. package/scripts/cpl-photos.js +1 -1
  78. package/scripts/cpl-tools.js +15 -1
  79. package/scripts/inattaxonphotos.js +1 -1
@@ -8,16 +8,19 @@ import { Images } from "../images.js";
8
8
  import imageSize from "image-size";
9
9
 
10
10
  class TaxonPage extends EBookPage {
11
+ #config;
11
12
  #taxon;
12
13
  #images;
13
14
 
14
15
  /**
15
16
  * @param {string} outputDir
17
+ * @param {Config} config
16
18
  * @param {import("../../taxonomy/taxon.js").Taxon} taxon
17
19
  * @param {Images} images
18
20
  */
19
- constructor(outputDir, taxon, images) {
21
+ constructor(outputDir, config, taxon, images) {
20
22
  super(outputDir + "/" + taxon.getFileName(), taxon.getName());
23
+ this.#config = config;
21
24
  this.#taxon = taxon;
22
25
  this.#images = images;
23
26
  }
@@ -40,19 +43,26 @@ class TaxonPage extends EBookPage {
40
43
  const name = this.#taxon.getName();
41
44
  let html = XHTML.textElement("h1", name);
42
45
 
46
+ // Show header with common name, native status, and family.
47
+ let header = "";
48
+
49
+ const cn = this.#taxon.getCommonNames();
50
+ if (cn && cn.length > 0) {
51
+ header += XHTML.textElement("div", cn.join(", "));
52
+ }
53
+
43
54
  const family = this.#taxon.getFamily();
44
- html += XHTML.wrap(
55
+ header += XHTML.wrap(
45
56
  "div",
46
57
  XHTML.getLink(family.getFileName(), family.getName()),
47
- { class: "section" },
48
58
  );
49
59
 
50
- const cn = this.#taxon.getCommonNames();
51
- if (cn && cn.length > 0) {
52
- html += XHTML.textElement("div", cn.join(", "), {
53
- class: "section",
54
- });
55
- }
60
+ header += XHTML.textElement(
61
+ "div",
62
+ this.#taxon.getStatusDescription(this.#config),
63
+ );
64
+
65
+ html += XHTML.wrap("div", header, "section hdr");
56
66
 
57
67
  html += HTMLTaxon.getFlowerInfo(this.#taxon, "section flr");
58
68
 
@@ -11,16 +11,19 @@ import { TaxonPage } from "./pages/taxonpage.js";
11
11
  import { TOCPage } from "./pages/tocpage.js";
12
12
 
13
13
  class PlantBook extends EBook {
14
+ #config;
14
15
  #taxa;
15
16
  #glossary;
16
17
  #images;
18
+ #maxTaxa;
17
19
 
18
20
  /**
19
21
  * @param {string} outputDir
20
22
  * @param {import("../config.js").Config} config
21
23
  * @param {import("../types.js").Taxa} taxa
24
+ * @param {number|undefined} maxTaxa
22
25
  */
23
- constructor(outputDir, config, taxa) {
26
+ constructor(outputDir, config, taxa, maxTaxa) {
24
27
  super(
25
28
  outputDir,
26
29
  getRequiredConfigValue(config, "filename"),
@@ -28,16 +31,21 @@ class PlantBook extends EBook {
28
31
  getRequiredConfigValue(config, "title"),
29
32
  );
30
33
 
34
+ this.#config = config;
31
35
  this.#taxa = taxa;
32
36
  const generator = new EBookSiteGenerator(config, this.getContentDir());
33
37
  this.#glossary = new GlossaryPages(generator);
34
38
  this.#images = new Images(generator, this.getContentDir(), taxa);
39
+ this.#maxTaxa = maxTaxa;
35
40
  }
36
41
 
37
42
  async createPages() {
38
43
  const contentDir = this.getContentDir();
39
44
 
40
- const taxonList = this.#taxa.getTaxonList();
45
+ const allTaxa = this.#taxa.getTaxonList();
46
+ const taxonList = this.#maxTaxa
47
+ ? allTaxa.slice(0, this.#maxTaxa)
48
+ : allTaxa;
41
49
 
42
50
  await this.#images.createImages(taxonList);
43
51
 
@@ -45,9 +53,15 @@ class PlantBook extends EBook {
45
53
  "creating taxon pages",
46
54
  taxonList.length,
47
55
  );
56
+
48
57
  for (let index = 0; index < taxonList.length; index++) {
49
58
  const taxon = taxonList[index];
50
- new TaxonPage(contentDir, taxon, this.#images).create();
59
+ new TaxonPage(
60
+ contentDir,
61
+ this.#config,
62
+ taxon,
63
+ this.#images,
64
+ ).create();
51
65
  meter.update(index + 1);
52
66
  }
53
67
  meter.stop();
@@ -30,6 +30,18 @@ export class ExternalSites {
30
30
  return new URL("https://www.calflora.org/app/taxon?crn=" + calfloraID);
31
31
  }
32
32
 
33
+ /**
34
+ * @param {import("./types.js").Taxon} taxon
35
+ * @returns {URL|undefined}
36
+ */
37
+ static getCalIPCRefLink(taxon) {
38
+ const calipcID = taxon.getCalIPCID();
39
+ if (!calipcID) {
40
+ return;
41
+ }
42
+ return new URL(`https://www.cal-ipc.org/plants/profile/${calipcID}/`);
43
+ }
44
+
33
45
  /**
34
46
  * @param {import("./types.js").Taxon} taxon
35
47
  * @returns {URL|undefined}
package/lib/htmltaxon.js CHANGED
@@ -74,6 +74,10 @@ const REFLINKS = {
74
74
  label: "Calflora",
75
75
  href: (taxon) => ExternalSites.getCalfloraRefLink(taxon),
76
76
  },
77
+ calipc: {
78
+ label: "Cal-IPC",
79
+ href: (taxon) => ExternalSites.getCalIPCRefLink(taxon),
80
+ },
77
81
  calscape: {
78
82
  label: "Calscape",
79
83
  href: (taxon) => ExternalSites.getCalscapeLink(taxon),
@@ -248,12 +252,19 @@ class HTMLTaxon {
248
252
  * @returns {string}
249
253
  */
250
254
  static getFooterHTML(taxon) {
251
- const footerTextPath = path.join(
255
+ const footerTextPathCommon = path.join(
252
256
  Config.getPackageDir(),
253
257
  "/data/text/",
254
258
  `${taxon.getBaseFileName()}.footer.md`,
255
259
  );
256
- return HTMLFragments.getMarkdownSection(footerTextPath);
260
+ const footerTextPathLocal = path.join(
261
+ "./data/footers/",
262
+ `${taxon.getBaseFileName()}.md`,
263
+ );
264
+ return (
265
+ HTMLFragments.getMarkdownSection(footerTextPathCommon) +
266
+ HTMLFragments.getMarkdownSection(footerTextPathLocal)
267
+ );
257
268
  }
258
269
 
259
270
  /**
package/lib/index.d.ts CHANGED
@@ -8,6 +8,7 @@ type PhotoRights = "CC0" | "CC BY" | "CC BY-NC" | "C" | null;
8
8
 
9
9
  type RefSourceCode =
10
10
  | "calflora"
11
+ | "calipc"
11
12
  | "calscape"
12
13
  | "cch"
13
14
  | "fna"
@@ -40,6 +41,7 @@ export type TaxonData = TaxonomyData & {
40
41
  CRPR: string;
41
42
  FESA: string;
42
43
  fna: string;
44
+ calipc: string;
43
45
  flower_color: string;
44
46
  GRank: string;
45
47
  "inat id": string;
@@ -229,6 +231,7 @@ export class Taxa<T> {
229
231
  extraTaxa?: TaxonOverrides[],
230
232
  extraSynonyms?: Record<string, string>[],
231
233
  );
234
+ static getIncludeList(dataDir: string): true | Record<string, TaxonData>;
232
235
  getTaxon(name: string): T;
233
236
  getTaxonList(): T[];
234
237
  }
package/lib/markdown.js CHANGED
@@ -2,7 +2,7 @@ import markdownIt from "markdown-it";
2
2
  import { Files } from "./files.js";
3
3
 
4
4
  export class Markdown {
5
- static #md = new markdownIt({ xhtmlOut: true });
5
+ static #md = new markdownIt({ html: true, xhtmlOut: true });
6
6
 
7
7
  /**
8
8
  * @param {string} filePath
package/lib/program.js CHANGED
@@ -1,7 +1,4 @@
1
1
  import { Command } from "commander";
2
- import { Files } from "./files.js";
3
- import { CSV } from "./csv.js";
4
- import path from "node:path";
5
2
 
6
3
  /**
7
4
  * @typedef {{
@@ -12,29 +9,6 @@ import path from "node:path";
12
9
  */
13
10
 
14
11
  class Program {
15
- /**
16
- * @param {string} dataDir
17
- */
18
- static getIncludeList(dataDir) {
19
- // Read inclusion list.
20
- const includeFileName = "taxa_include.csv";
21
- const includeFilePath = dataDir + "/" + includeFileName;
22
- if (!Files.exists(includeFilePath)) {
23
- console.log(includeFilePath + " not found; loading all taxa");
24
- return true;
25
- }
26
-
27
- /** @type {import("./index.js").TaxonData[]} */
28
- // @ts-ignore
29
- const includeCSV = CSV.readFile(path.join(dataDir, includeFileName));
30
- /** @type {Object<string,import("./index.js").TaxonData>} */
31
- const include = {};
32
- for (const row of includeCSV) {
33
- include[row["taxon_name"]] = row;
34
- }
35
- return include;
36
- }
37
-
38
12
  static getProgram() {
39
13
  const program = new Command();
40
14
  program
@@ -56,16 +56,16 @@ export class SiteGenerator {
56
56
  Config.getPackageDir() + "/data/illustrations/inkscape";
57
57
  const entries = Files.getDirEntries(srcDir);
58
58
  for (const entry of entries) {
59
- const srcFile = Files.join(srcDir, entry);
59
+ const srcFile = path.join(srcDir, entry);
60
60
  const srcSVG = Files.read(srcFile);
61
61
  const result = optimize(srcSVG, {
62
- plugins: ["preset-default", "removeDimensions"],
62
+ enable: ["removeDimensions"],
63
63
  });
64
- Files.write(Files.join(outputDir, entry), result.data);
64
+ Files.write(path.join(outputDir, entry), result.data);
65
65
  }
66
66
  }
67
67
 
68
- const outputDir = Files.join(this.#baseDir, "i");
68
+ const outputDir = path.join(this.#baseDir, "i");
69
69
  Files.mkdir(outputDir);
70
70
 
71
71
  optimizeSVG(outputDir);
@@ -9,8 +9,8 @@ import { Families } from "./families.js";
9
9
  import { FlowerColor } from "../flowercolor.js";
10
10
  import { TaxaCSV } from "../tools/taxacsv.js";
11
11
  import { ErrorLog } from "../errorlog.js";
12
- import { Program } from "../program.js";
13
12
  import { Photo } from "../photo.js";
13
+ import { Files } from "../files.js";
14
14
 
15
15
  /**
16
16
  * @typedef {{Current: string;Former: string;Type: string;}} SynonymData
@@ -28,7 +28,7 @@ const FLOWER_COLORS = [
28
28
  { name: "brown", color: "brown" },
29
29
  ];
30
30
 
31
- class Taxa {
31
+ export class Taxa {
32
32
  #families;
33
33
  #errorLog;
34
34
  /** @type {Object<string,Taxon>} */
@@ -152,6 +152,29 @@ class Taxa {
152
152
  );
153
153
  }
154
154
 
155
+ /**
156
+ * @param {string} dataDir
157
+ * @returns {true|Object<string,import("../index.js").TaxonData>}
158
+ */
159
+ static getIncludeList(dataDir) {
160
+ // Read inclusion list.
161
+ const includeFileName = "taxa_include.csv";
162
+ const includeFilePath = dataDir + "/" + includeFileName;
163
+ if (!Files.exists(includeFilePath)) {
164
+ console.log(includeFilePath + " not found; loading all taxa");
165
+ return true;
166
+ }
167
+
168
+ /** @type {import("../index.js").TaxonData[]} */
169
+ const includeCSV = CSV.readFile(path.join(dataDir, includeFileName));
170
+ /** @type {Object<string,import("../index.js").TaxonData>} */
171
+ const include = {};
172
+ for (const row of includeCSV) {
173
+ include[row["taxon_name"]] = row;
174
+ }
175
+ return include;
176
+ }
177
+
155
178
  /**
156
179
  * @param {string} name
157
180
  */
@@ -193,7 +216,7 @@ class Taxa {
193
216
  taxa = await taxaLoaderClass.TaxaLoader.loadTaxa(options, errorLog);
194
217
  } else {
195
218
  taxa = new Taxa(
196
- Program.getIncludeList(options.datadir),
219
+ Taxa.getIncludeList(options.datadir),
197
220
  errorLog,
198
221
  options.showFlowerErrors,
199
222
  );
@@ -297,5 +320,3 @@ class Taxa {
297
320
  }
298
321
  }
299
322
  }
300
-
301
- export { Taxa };
@@ -13,6 +13,7 @@ class Taxon extends Taxonomy {
13
13
  #cch2id;
14
14
  #fnaName;
15
15
  #calscapeCN;
16
+ #calipcID;
16
17
  #lifeCycle;
17
18
  #flowerColors;
18
19
  #bloomStart;
@@ -51,6 +52,7 @@ class Taxon extends Taxonomy {
51
52
  this.#iNatID = data["inat id"];
52
53
  this.#cch2id = data.cch2_id;
53
54
  this.#fnaName = data.fna ?? "";
55
+ this.#calipcID = data.calipc ?? "";
54
56
  this.#calscapeCN =
55
57
  data.calscape_cn === "" ? undefined : data.calscape_cn;
56
58
  this.#lifeCycle = data.life_cycle;
@@ -118,12 +120,20 @@ class Taxon extends Taxonomy {
118
120
  return this.#bloomStart;
119
121
  }
120
122
 
123
+ getCalfloraID() {
124
+ return this.#calRecNum;
125
+ }
126
+
121
127
  getCalfloraName() {
122
128
  return this.getName().replace(" subsp.", " ssp.").replace("×", "X");
123
129
  }
124
130
 
125
- getCalfloraID() {
126
- return this.#calRecNum;
131
+ getCalIPCID() {
132
+ return this.#calipcID;
133
+ }
134
+
135
+ getCalIPCName() {
136
+ return this.getName().replace(" subsp.", " ssp.");
127
137
  }
128
138
 
129
139
  getCalscapeCommonName() {
@@ -70,12 +70,10 @@ export class Calflora {
70
70
  );
71
71
 
72
72
  /** @type {CalfloraData[]} */
73
- // @ts-ignore
74
73
  const csvActive = CSV.readFile(
75
74
  path.join(toolsDataPath, calfloraDataFileNameActive),
76
75
  );
77
76
  /** @type {CalfloraData[]} */
78
- // @ts-ignore
79
77
  const csvCounties = CSV.readFile(
80
78
  path.join(toolsDataPath, calfloraDataFileNameCounties),
81
79
  );
@@ -0,0 +1,111 @@
1
+ import * as path from "path";
2
+ import { Files } from "../files.js";
3
+ import { TaxaCSV } from "./taxacsv.js";
4
+ import { scrape } from "@htmltools/scrape";
5
+
6
+ export class CalIPC {
7
+ /**
8
+ * @param {string} toolsDataDir
9
+ * @param {string} dataDir
10
+ * @param {import("../types.js").Taxa} taxa
11
+ * @param {import("../exceptions.js").Exceptions} exceptions
12
+ * @param {import("../errorlog.js").ErrorLog} errorLog
13
+ * @param {boolean} update
14
+ */
15
+ static async analyze(
16
+ toolsDataDir,
17
+ dataDir,
18
+ taxa,
19
+ exceptions,
20
+ errorLog,
21
+ update,
22
+ ) {
23
+ const toolsDataPath = toolsDataDir + "/calipc";
24
+ // Create data directory if it's not there.
25
+ Files.mkdir(toolsDataPath);
26
+
27
+ const calipcFileName = path.join(toolsDataPath, "inventory.html");
28
+ if (!Files.exists(calipcFileName)) {
29
+ console.info("retrieving " + calipcFileName);
30
+ await Files.fetch(
31
+ "https://www.cal-ipc.org/plants/inventory/",
32
+ calipcFileName,
33
+ );
34
+ }
35
+
36
+ const parsed = scrape.parseFile(calipcFileName);
37
+ const links = scrape.getSubtrees(parsed, (e) => {
38
+ if (e.tagName !== "td") {
39
+ return false;
40
+ }
41
+ const className = scrape.getAttr(e, "class");
42
+ return className === "it-latin";
43
+ });
44
+
45
+ /** @type {Map<string,string>} */
46
+ const calipcTaxa = new Map();
47
+ for (const link of links) {
48
+ const name = scrape.getTextContent(link);
49
+ const url = scrape.getAttr(link.children[0], "href");
50
+ if (!url) {
51
+ console.warn(`Cal-IPC url not found for ${name}`);
52
+ continue;
53
+ }
54
+ const id = url.match(/\/profile\/(.*)\//);
55
+ if (!id) {
56
+ console.warn(`Cal-IPC url mismatch for ${url}`);
57
+ continue;
58
+ }
59
+ calipcTaxa.set(name, id[1]);
60
+ }
61
+
62
+ const idsToUpdate = new Map();
63
+
64
+ for (const taxon of taxa.getTaxonList()) {
65
+ const name = taxon.getName();
66
+ if (name.includes(" unknown")) {
67
+ continue;
68
+ }
69
+ const calipcName = taxon.getCalIPCName();
70
+ const calipcData = calipcTaxa.get(calipcName);
71
+ if (!calipcData) {
72
+ continue;
73
+ }
74
+
75
+ // Check native status.
76
+ if (taxon.isCANative()) {
77
+ errorLog.log(name, "is native but listed in Cal-IPC");
78
+ }
79
+
80
+ if (calipcData !== taxon.getCalIPCID()) {
81
+ errorLog.log(
82
+ name,
83
+ "Cal-IPC ID in Cal-IPC is different than taxa.csv",
84
+ calipcData,
85
+ taxon.getCalIPCID(),
86
+ );
87
+ idsToUpdate.set(name, calipcData);
88
+ }
89
+ }
90
+
91
+ if (update) {
92
+ this.#updateIds(dataDir, idsToUpdate);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * @param {string} dataDir
98
+ * @param {Map<string,string>} idsToUpdate
99
+ */
100
+ static #updateIds(dataDir, idsToUpdate) {
101
+ const taxa = new TaxaCSV(dataDir);
102
+ for (const taxonData of taxa.getTaxa()) {
103
+ const id = idsToUpdate.get(taxonData.taxon_name);
104
+ if (!id) {
105
+ continue;
106
+ }
107
+ taxonData["calipc"] = id;
108
+ }
109
+ taxa.write();
110
+ }
111
+ }
@@ -5,16 +5,17 @@ const HEADERS = [
5
5
  "taxon_name",
6
6
  "common name",
7
7
  "status",
8
+ "life_cycle",
9
+ "flower_color",
10
+ "bloom_start",
11
+ "bloom_end",
8
12
  "jepson id",
9
13
  "calrecnum",
10
14
  "inat id",
11
15
  "cch2_id",
12
16
  "fna",
13
17
  "calscape_cn",
14
- "life_cycle",
15
- "flower_color",
16
- "bloom_start",
17
- "bloom_end",
18
+ "calipc",
18
19
  "RPI ID",
19
20
  "CRPR",
20
21
  "CESA",
@@ -66,6 +66,8 @@ export class EleventyGenerator extends SiteGenerator {
66
66
  const passThroughPatterns = [
67
67
  "assets",
68
68
  "i",
69
+ "calflora_list_*.txt",
70
+ "inat_list_*.txt",
69
71
  "errors.tsv",
70
72
  ...generator.getPassThroughPatterns(),
71
73
  ];
@@ -69,7 +69,11 @@ export function convertToCSVPhoto(apiPhoto) {
69
69
  */
70
70
  async function fetchInatTaxa(inatTaxonIDs) {
71
71
  const url = `https://api.inaturalist.org/v2/taxa/${inatTaxonIDs.join(",")}?fields=(taxon_photos:(photo:(medium_url:!t,attribution:!t,license_code:!t)))`;
72
- const resp = await fetch(url);
72
+ const resp = await getResponse(url);
73
+ if (resp instanceof Error) {
74
+ console.error(`unable to fetch taxa: ${resp.message}`);
75
+ return [];
76
+ }
73
77
  if (!resp.ok) {
74
78
  const error = await resp.text();
75
79
  throw new Error(`Failed to fetch taxa from iNat: ${error}`);
@@ -163,7 +167,7 @@ export async function getObsPhotosForTaxa(taxaToUpdate, locationOptions) {
163
167
  continue;
164
168
  }
165
169
 
166
- // Just get the CC-licensed ones, 5 per taxon should be fine (max is 20 on iNat). Whether or not
170
+ // Just get the CC-licensed ones, 5 per taxon should be fine (max is 20 on iNat).
167
171
  const rawPhotoInfo = observations
168
172
  .map((obs) =>
169
173
  obs.observation_photos.map((op) => {
@@ -182,7 +186,7 @@ export async function getObsPhotosForTaxa(taxaToUpdate, locationOptions) {
182
186
  if (!obj) {
183
187
  continue;
184
188
  }
185
- processedPhotoInfo.push(obj);
189
+ processedPhotoInfo.push({ obsId: photo.obsId.toString(), ...obj });
186
190
  }
187
191
  photos.set(taxon.getName(), processedPhotoInfo);
188
192
 
@@ -270,7 +274,7 @@ function getAttribution(rawAttribution) {
270
274
 
271
275
  let lastQueryTime = Date.now();
272
276
  /**
273
- * @param {URL} url
277
+ * @param {URL|string} url
274
278
  * @returns {Promise<Response|Error>}
275
279
  */
276
280
  async function getResponse(url) {
@@ -35,6 +35,7 @@ export class PageTaxon extends GenericPage {
35
35
  HTMLTaxon.addRefLink(links, this.#taxon, "fna");
36
36
  HTMLTaxon.addRefLink(links, this.#taxon, "cch");
37
37
  HTMLTaxon.addRefLink(links, this.#taxon, "calscape");
38
+ HTMLTaxon.addRefLink(links, this.#taxon, "calipc");
38
39
  return links;
39
40
  }
40
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ca-plant-list/ca-plant-list",
3
- "version": "0.4.33",
3
+ "version": "0.4.36",
4
4
  "description": "Tools to create files for a website listing plants in an area of California.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -40,14 +40,14 @@
40
40
  "@htmltools/scrape": "^0.1.1",
41
41
  "archiver": "^5.3.1",
42
42
  "cli-progress": "^3.12.0",
43
- "commander": "^13.1.0",
43
+ "commander": "^14.0.1",
44
44
  "csv-parse": "^5.6.0",
45
45
  "csv-stringify": "^6.5.2",
46
46
  "exceljs": "^4.4.0",
47
47
  "image-size": "^1.1.1",
48
48
  "markdown-it": "^14.1.0",
49
49
  "sharp": "^0.33.5",
50
- "svgo-ll": "^5.6.0",
50
+ "svgo-ll": "^6.0.1",
51
51
  "unzipper": "^0.12.3"
52
52
  },
53
53
  "devDependencies": {
@@ -57,10 +57,10 @@
57
57
  "@types/markdown-it": "^14.1.2",
58
58
  "@types/node": "^22.10.7",
59
59
  "@types/unzipper": "^0.10.9",
60
- "eslint": "^9.20.1",
60
+ "eslint": "^9.35.0",
61
61
  "jest": "^29.7.0",
62
- "prettier": "^3.5.1",
62
+ "prettier": "^3.6.2",
63
63
  "puppeteer": "^24.1.1",
64
- "typescript": "^5.7.3"
64
+ "typescript": "^5.9.2"
65
65
  }
66
66
  }
@@ -8,9 +8,16 @@ import { Program } from "../lib/program.js";
8
8
  import { Taxa } from "../lib/taxonomy/taxa.js";
9
9
 
10
10
  const program = Program.getProgram();
11
- program
12
- .option("-l, --locationsdir <dir>", "directory containing location data")
13
- .action(build);
11
+ program.option(
12
+ "-l, --locationsdir <dir>",
13
+ "directory containing location data",
14
+ );
15
+ program.option(
16
+ "--location <name>",
17
+ "name of location to generate (otherwise all ebooks will be generated)",
18
+ );
19
+ program.option("--max-taxa <number>", "maximum number of taxa to include");
20
+ program.action(build);
14
21
 
15
22
  await program.parseAsync();
16
23
 
@@ -26,6 +33,11 @@ async function build(options) {
26
33
  const outputBase = options.outputdir;
27
34
  const subdirs = Files.getDirEntries(locationsDir);
28
35
  for (const subdir of subdirs) {
36
+ // If a single location was specified, ignore the others.
37
+ if (options.location && subdir !== options.location) {
38
+ continue;
39
+ }
40
+
29
41
  console.log("Generating " + subdir);
30
42
  const suffix = "/" + subdir;
31
43
  const path = locationsDir + suffix;
@@ -34,6 +46,7 @@ async function build(options) {
34
46
  outputBase + suffix,
35
47
  path,
36
48
  options.showFlowerErrors,
49
+ options.maxTaxa,
37
50
  );
38
51
  }
39
52
  }
@@ -43,6 +56,7 @@ async function build(options) {
43
56
  options.outputdir,
44
57
  options.datadir,
45
58
  options.showFlowerErrors,
59
+ options.maxTaxa,
46
60
  );
47
61
  }
48
62
  }
@@ -51,18 +65,19 @@ async function build(options) {
51
65
  * @param {string} outputDir
52
66
  * @param {string} dataDir
53
67
  * @param {boolean} showFlowerErrors
68
+ * @param {number|undefined} maxTaxa
54
69
  */
55
- async function buildBook(outputDir, dataDir, showFlowerErrors) {
70
+ async function buildBook(outputDir, dataDir, showFlowerErrors, maxTaxa) {
56
71
  const errorLog = new ErrorLog(outputDir + "/errors.tsv");
57
72
 
58
73
  const taxa = new Taxa(
59
- Program.getIncludeList(dataDir),
74
+ Taxa.getIncludeList(dataDir),
60
75
  errorLog,
61
76
  showFlowerErrors,
62
77
  );
63
78
 
64
79
  const config = new Config(dataDir);
65
- const ebook = new PlantBook(outputDir, config, taxa);
80
+ const ebook = new PlantBook(outputDir, config, taxa, maxTaxa);
66
81
  await ebook.create();
67
82
  errorLog.write();
68
83
  }