@ca-plant-list/ca-plant-list 0.4.13 → 0.4.15

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.
@@ -0,0 +1 @@
1
+ A bristle-like structure projecting from a [lemma](./lemma.html) or [glume](./glume.html).
@@ -0,0 +1,4 @@
1
+ A floret is a small flower on a grass [spikelet](./spikelet.html). The floret is composed of:
2
+
3
+ - A [lemma](./lemma.html) at the base of the floret.
4
+ - A [palea](./palea.html), usually smaller than and partially enclosed by the lemma.
@@ -0,0 +1 @@
1
+ In a grass [spikelet](./spikelet.html), the glume is a bract below all the individual flowers. Each spikelet generally has 2 glumes, one slightly below the other; the lower glume is usually shorter than the upper.
@@ -0,0 +1,5 @@
1
+ A bract at the base of a [grass flower](./floret.html) (floret). The lemma is often visually similar to the [glumes](./glume.html), and often has striped veins running along its length.
2
+
3
+ The lemma may contain an [awn](./awn.html) (a bristle-like structure projecting beyond the lemma).
4
+
5
+ The lemma is fertile if there is a flower above it (attached to the [rachilla](./rachilla.html) at the same point as the lemma), otherwise it is sterile.
@@ -0,0 +1 @@
1
+ A bract at the base of a [grass flower](./floret.html) (floret).
@@ -0,0 +1 @@
1
+ In a grass [spikelet](./spikelet.html), the central stem to which other structures are attached.
@@ -0,0 +1,5 @@
1
+ In grasses, a structure holding one or more flowers. The spikelet contains:
2
+
3
+ - A [rachilla](./rachilla.html) (central stem) to which the flowers and other structures are attached.
4
+ - 2 [glumes](./glume.html) below all the flowers.
5
+ - One or more [flowers](./floret.html) (florets) above the glumes.
package/data/synonyms.csv CHANGED
@@ -1515,7 +1515,7 @@ Phlox gracilis,Microsteris gracilis
1515
1515
  Phoradendron macrophyllum,Phoradendron leucarpum subsp. macrophyllum,INAT
1516
1516
  Phoradendron serotinum subsp. macrophyllum,Phoradendron leucarpum subsp. macrophyllum
1517
1517
  Phoradendron serotinum subsp. tomentosum,Phoradendron leucarpum subsp. tomentosum
1518
- Phoradendron villosum,Phoradendron leucarpum subsp. tomentosum
1518
+ Phoradendron villosum,Phoradendron leucarpum subsp. tomentosum,INAT
1519
1519
  Phragmites australis var. berlandieri,Phragmites australis
1520
1520
  Phragmites berlandieri,Phragmites australis
1521
1521
  Phragmites communis var. berlandieri,Phragmites australis
package/data/taxa.csv CHANGED
@@ -971,7 +971,7 @@ Juncus phaeocephalus var. phaeocephalus,brown-headed rush,N,60393,4491,61036,Bro
971
971
  Juncus tenuis,poverty rush,N,29725,4496,69930,Poverty Rush
972
972
  Juncus xiphioides,iris-leaved rush,N,29743,4502,57110,Irisleaf Rush
973
973
  Juniperus californica,California juniper,N,29749,4503,57889,California Juniper
974
- Juniperus communis var. saxatilis,mountain juniper,N,60424,10933,,Common Juniper
974
+ Juniperus communis var. saxatilis,mountain juniper,N,60424,10933,81001,Common Juniper
975
975
  Keckiella breviflora var. breviflora,,N,75718,4520,59015,Gaping Keckiella
976
976
  Keckiella corymbosa,,N,29883,4523,62043,Keckiella
977
977
  Kickxia elatine,fluellin,X,29898,4532,64332
@@ -1144,7 +1144,7 @@ Maianthemum racemosum,false Solomon's seal,N,32495,10096,51062,Large False Solom
1144
1144
  Maianthemum stellatum,,N,32497,9629,53268,False Solomon Seal
1145
1145
  Malacothamnus arcuatus var. elmeri,,N,108345,14681,1497404,Eastern Bewildering Bushmallow,perennial,pink,4,5,1065,1B.2,,,S2,G2
1146
1146
  Malacothamnus fremontii var. fremontii,long-haired unfurled bushmallow,N,108341,14689,70398,,perennial,pink,5,6
1147
- Malacothamnus hallii,,N,108345,14681,1497404,,,,,,1065,1B.2,,,S2,G2
1147
+ Malacothamnus hallii,Hall's bushmallow,N,108345,14681,1497404,,perennial,pink,4,5,1065,1B.2,,,S2,G2
1148
1148
  Malacothrix californica,,N,4059,5325,58026,California Desertdandelion
1149
1149
  Malacothrix clevelandii,Cleveland's malacothrix,N,4060,5326,58841,Cleveland's Desertdandelion
1150
1150
  Malacothrix coulteri,snake's-head,N,4061,5327,58027,Snake's Head
@@ -1347,8 +1347,8 @@ Phacelia ramosissima,branching phacelia,N,37557,6389,50617,Branching Phacelia
1347
1347
  Phacelia rattanii,,N,37563,6396,58925,Rattan's Phacelia,,"white,blue",5,7
1348
1348
  Phacelia suaveolens,sweet scented phacelia,N,37577,6402,68816,Sweetscented Phacelia
1349
1349
  Phacelia tanacetifolia,tansy-leaf phacelia,N,37579,6405,58185,Lacy Phacelia
1350
- Phalaris angusta,timothy canary grass,N,37600,6415,78534,Timothy Canary Grass
1351
- Phalaris aquatica,Harding grass,X,37601,6416,57188
1350
+ Phalaris angusta,timothy canary grass,N,37600,6415,78534,Timothy Canary Grass,annual,,5,6
1351
+ Phalaris aquatica,Harding grass,X,37601,6416,57188,,annual,,4,8
1352
1352
  Phalaris arundinacea,reed canary grass,N,37604,6417,63337,Reed Canarygrass
1353
1353
  Phalaris brachystachys,short-spiked canary grass,X,37605,6418,78535
1354
1354
  Phalaris californica,California canary grass,N,37606,6419,57192,California Canarygrass
@@ -1360,7 +1360,7 @@ Phoenix canariensis,Canary Island date palm,X,37878,6449,78554
1360
1360
  Pholistoma auritum var. auritum,fiesta flower,N,63599,6455,58926,Fiesta Flower
1361
1361
  Pholistoma membranaceum,white fiesta flower,N,37886,6456,57352,White Fiesta Flower
1362
1362
  Phoradendron leucarpum subsp. macrophyllum,big leaf mistletoe,N,98417,13230,166732,Colorado Desert Mistletoe
1363
- Phoradendron leucarpum subsp. tomentosum,oak mistletoe,N,98416,13231,545020,Pacific Mistletoe
1363
+ Phoradendron leucarpum subsp. tomentosum,oak mistletoe,N,98416,13231,343310,Pacific Mistletoe
1364
1364
  Phragmites australis,common reed,N,37931,6465,64237,Common Reed
1365
1365
  Phyla lanceolata,,N,37942,6466,59042,Lanceleaf Fogfruit
1366
1366
  Phyla nodiflora,,N,37943,6467,59040,Common Lippia,perennial,white,5,11
@@ -1526,7 +1526,7 @@ Ranunculus orthorhynchus var. bloomeri,,N,64956,7051,81337,Bloomer's Buttercup
1526
1526
  Ranunculus orthorhynchus var. orthorhynchus,,N,64957,7052,81338,Straightbeak Buttercup
1527
1527
  Ranunculus repens,,X,40965,7056,48229
1528
1528
  Ranunculus sceleratus,cursed crowsfoot,N,40971,7059,59301,Cursed Buttercup
1529
- Ranunculus sceleratus var. sceleratus,,X,64987,11983,,,annual,yellow,4,6
1529
+ Ranunculus sceleratus var. sceleratus,,X,64987,11983,81341,,annual,yellow,4,6
1530
1530
  Raphanus raphanistrum,jointed charlock,X,40991,7063,55411
1531
1531
  Raphanus sativus,wild radish,X,40992,7064,995125
1532
1532
  Rhamnus alaternus,Italian buckthorn,X,81104,9447,82856
@@ -0,0 +1,6 @@
1
+ ## Resources
2
+
3
+ - Gowen, David. “[TWO NEW SPECIES OF ERIASTRUM (POLEMONIACEAE) FROM CALIFORNIA.](http://www.jstor.org/stable/24621044)” Journal of the Botanical Research Institute of Texas, vol. 7, no. 1, 2013, pp. 21–24. JSTOR
4
+ - California Department of Fish and Wildlife (CDFW). 2023. [Report to the Fish and Game
5
+ Commission, status review of Lime Ridge eriastrum (_Eriastrum ertterae_)](https://nrm.dfg.ca.gov/FileHandler.ashx?DocumentID=213638&inline). California Department
6
+ of Fish and Wildlife
@@ -0,0 +1,3 @@
1
+ ## Resources
2
+
3
+ - Morse, Keir (2023). [Malacothamnus Volume 3 - A Revised Treatment of the Genus Malacothamnus (Malvaceae) Based on Morphological and Phylogenetic Evidence](https://doi.org/10.6084/m9.figshare.23937066.v1). figshare. Book.
@@ -0,0 +1,3 @@
1
+ ## Resources
2
+
3
+ - Morse, Keir (2023). [Malacothamnus Volume 3 - A Revised Treatment of the Genus Malacothamnus (Malvaceae) Based on Morphological and Phylogenetic Evidence](https://doi.org/10.6084/m9.figshare.23937066.v1). figshare. Book.
@@ -0,0 +1,3 @@
1
+ ## Resources
2
+
3
+ - Morse, Keir (2023). [Malacothamnus Volume 3 - A Revised Treatment of the Genus Malacothamnus (Malvaceae) Based on Morphological and Phylogenetic Evidence](https://doi.org/10.6084/m9.figshare.23937066.v1). figshare. Book.
@@ -1,152 +1,152 @@
1
1
  /* Defaults */
2
2
 
3
3
  :root {
4
- /* See https://visme.co/blog/website-color-schemes/ theme 4 */
5
- --bs-body-bg: #5cdb95;
6
- --bs-link-color: #05386b;
7
- --pl-navbar-bg: #379683;
4
+ /* See https://visme.co/blog/website-color-schemes/ theme 4 */
5
+ --bs-body-bg: #5cdb95;
6
+ --bs-link-color: #05386b;
7
+ --pl-navbar-bg: #379683;
8
8
  }
9
9
 
10
10
  .navbar-nav {
11
- --bs-nav-link-hover-color: rgba(255, 255, 255, .5);
11
+ --bs-nav-link-hover-color: rgba(255, 255, 255, .5);
12
12
  }
13
13
 
14
14
  .navbar {
15
- --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='white' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
16
- --bs-navbar-color: white;
17
- --bs-navbar-brand-hover-color: rgba(255, 255, 255, .5);
18
- --bs-navbar-brand-color: white;
19
- --bs-navbar-nav-link-padding-x: 1rem;
20
- background-color: var(--pl-navbar-bg);
15
+ --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='white' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
16
+ --bs-navbar-color: white;
17
+ --bs-navbar-brand-hover-color: rgba(255, 255, 255, .5);
18
+ --bs-navbar-brand-color: white;
19
+ --bs-navbar-nav-link-padding-x: 1rem;
20
+ background-color: var(--pl-navbar-bg);
21
21
  }
22
22
 
23
23
  p {
24
- margin-bottom: .5rem;
24
+ margin-bottom: .5rem;
25
25
  }
26
26
 
27
27
  span.label {
28
- font-weight: 600;
29
- padding-right: .5rem;
28
+ font-weight: 600;
29
+ padding-right: .5rem;
30
30
  }
31
31
 
32
32
  td {
33
- border: solid 1px;
34
- padding: 0 .5rem;
35
- vertical-align: top;
33
+ border: solid 1px;
34
+ padding: 0 .5rem;
35
+ vertical-align: top;
36
36
  }
37
37
 
38
38
  th {
39
- vertical-align: bottom;
39
+ vertical-align: bottom;
40
40
  }
41
41
 
42
42
  ul.listmenu {
43
- display: flex;
44
- padding: 0;
43
+ display: flex;
44
+ padding: 0;
45
45
  }
46
46
 
47
47
  ul.listmenu li {
48
- display: inline-block;
49
- padding: 0;
50
- padding-right: .25em;
48
+ display: inline-block;
49
+ padding: 0;
50
+ padding-right: .25em;
51
51
  }
52
52
 
53
53
  ul.listmenu li::after {
54
- content: " |";
54
+ content: " |";
55
55
  }
56
56
 
57
57
  ul.listmenu li:last-child:after {
58
- content: "";
58
+ content: "";
59
59
  }
60
60
 
61
61
  /* Utilities */
62
62
  .right {
63
- text-align: right;
63
+ text-align: right;
64
64
  }
65
65
 
66
66
  /* Lists */
67
67
 
68
68
  span.native {
69
- font-weight: bold;
69
+ font-weight: bold;
70
70
  }
71
71
 
72
72
  span.rare::before {
73
- content: "\2606";
73
+ content: "\2606";
74
74
  }
75
75
 
76
76
  /* Glossary */
77
77
  div.glossary img {
78
- max-height: 300px;
78
+ max-height: 300px;
79
79
  }
80
80
 
81
81
  /* Taxon Page */
82
82
 
83
83
  .common-names {
84
- font-size: larger;
85
- font-weight: bold;
84
+ font-size: larger;
85
+ font-weight: bold;
86
86
  }
87
87
 
88
88
  .native-status {
89
- font-weight: 600;
89
+ font-weight: 600;
90
90
  }
91
91
 
92
92
  div.grid {
93
- display: grid;
94
- grid-auto-flow: row;
95
- grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
96
- column-gap: 2rem;
93
+ display: grid;
94
+ grid-auto-flow: row;
95
+ grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
96
+ column-gap: 2rem;
97
97
  }
98
98
 
99
99
  div.grid.borders>div.section {
100
- border: solid 1px;
101
- border-radius: 3%;
102
- padding: .5rem;
100
+ border: solid 1px;
101
+ border-radius: 3%;
102
+ padding: .5rem;
103
103
  }
104
104
 
105
105
  div.wrapper {
106
- display: flex;
107
- flex-wrap: wrap;
108
- column-gap: 4rem;
106
+ display: flex;
107
+ flex-wrap: wrap;
108
+ column-gap: 4rem;
109
109
  }
110
110
 
111
111
  div.section {
112
- margin-bottom: .5rem;
112
+ margin-bottom: .5rem;
113
113
  }
114
114
 
115
115
  div.section h2 {
116
- font-size: 1.25rem;
116
+ font-size: 1.25rem;
117
117
  }
118
118
 
119
119
  div.section.nobullet ul {
120
- margin: 0;
121
- padding: 0;
120
+ margin: 0;
121
+ padding: 0;
122
122
  }
123
123
 
124
124
  div.section.nobullet li {
125
- display: block;
126
- margin-left: 1rem;
127
- text-indent: -1rem;
125
+ display: block;
126
+ padding-left: .5rem;
127
+ text-indent: -.5rem;
128
128
  }
129
129
 
130
130
  div.section ul.indent {
131
- padding-left: 1rem;
131
+ padding-left: 1rem;
132
132
  }
133
133
 
134
134
  img.flr-color {
135
- margin-bottom: .2rem;
136
- margin-right: .5rem;
137
- width: 1rem;
135
+ margin-bottom: .2rem;
136
+ margin-right: .5rem;
137
+ width: 1rem;
138
138
  }
139
139
 
140
140
  span.flr-time,
141
141
  span.lc {
142
- font-weight: bold;
142
+ font-weight: bold;
143
143
  }
144
144
 
145
145
  span.lcs {
146
- margin-right: 1rem;
146
+ margin-right: 1rem;
147
147
  }
148
148
 
149
149
  /* Forms */
150
150
  input {
151
- margin-right: .5em;
151
+ margin-right: .5em;
152
152
  }
package/lib/html.js CHANGED
@@ -6,7 +6,7 @@ export class HTML {
6
6
  static arrayToLI(items) {
7
7
  return items.reduce(
8
8
  (itemHTML, currVal) => itemHTML + "<li>" + currVal + "</li>",
9
- ""
9
+ "",
10
10
  );
11
11
  }
12
12
 
@@ -107,7 +107,7 @@ export class HTML {
107
107
  /**
108
108
  * @param {string} elName
109
109
  * @param {string} text
110
- * @param {Object<string,string>} attributes
110
+ * @param {Object<string,string>} [attributes]
111
111
  */
112
112
  static textElement(elName, text, attributes = {}) {
113
113
  return HTML.#getElement(elName, text, attributes, true);
package/lib/htmltaxon.js CHANGED
@@ -1,5 +1,7 @@
1
+ import { Config } from "./config.js";
1
2
  import { DateUtils } from "./dateutils.js";
2
3
  import { HTML } from "./html.js";
4
+ import { Markdown } from "./markdown.js";
3
5
  import { RarePlants } from "./rareplants.js";
4
6
  import { TextUtils } from "./textutils.js";
5
7
 
@@ -126,6 +128,19 @@ class HTMLTaxon {
126
128
  : HTML.wrap("div", parts.join(""), { class: classNames });
127
129
  }
128
130
 
131
+ /**
132
+ * @param {Taxon} taxon
133
+ * @returns {string}
134
+ */
135
+ static getFooterHTML(taxon) {
136
+ const footerTextPath =
137
+ Config.getPackageDir() +
138
+ "/data/text/" +
139
+ taxon.getBaseFileName() +
140
+ ".footer.md";
141
+ return HTMLTaxon.getMarkdownSection(footerTextPath);
142
+ }
143
+
129
144
  /**
130
145
  * @param {Taxon} taxon
131
146
  */
@@ -136,6 +151,36 @@ class HTMLTaxon {
136
151
  );
137
152
  }
138
153
 
154
+ /**
155
+ * @param {string[]} list
156
+ * @param {string} header
157
+ * @param {string} [className]
158
+ */
159
+ static getListSectionHTML(list, header, className) {
160
+ let html = "";
161
+ if (list.length > 0) {
162
+ html += `<div class="section nobullet${className ? " " + className : ""}">`;
163
+ html += HTML.textElement("h2", header);
164
+ html += "<ul>";
165
+ html += HTML.arrayToLI(list);
166
+ html += "</ul>";
167
+ html += "</div>";
168
+ }
169
+ return html;
170
+ }
171
+
172
+ /**
173
+ * @param {string} filePath
174
+ * @returns {string}
175
+ */
176
+ static getMarkdownSection(filePath) {
177
+ const footerMarkdown = Markdown.fileToHTML(filePath);
178
+ if (footerMarkdown) {
179
+ return HTML.wrap("div", footerMarkdown, "section");
180
+ }
181
+ return "";
182
+ }
183
+
139
184
  /**
140
185
  * @param {Taxon[]} taxa
141
186
  * @param {TaxaCol[]} [columns]
package/lib/index.d.ts CHANGED
@@ -38,6 +38,40 @@ export class Files {
38
38
  static write(fileName: string, data: string, overwrite: boolean): void;
39
39
  }
40
40
 
41
+ export class HTML {
42
+ static arrayToLI(items: string[]): string;
43
+ static getLink(
44
+ href: string | undefined,
45
+ linkText: string,
46
+ attrs?: Record<string, string> | string,
47
+ openInNewWindow?: boolean,
48
+ ): string;
49
+ static textElement(
50
+ elName: string,
51
+ text: string,
52
+ attrs?: Record<string, string>,
53
+ ): string;
54
+ static wrap(
55
+ elName: string,
56
+ text: string,
57
+ attrs?: string | Record<string, string> | undefined,
58
+ ): string;
59
+ }
60
+
61
+ export class HTMLTaxon {
62
+ static getFooterHTML(taxon: Taxon): string;
63
+ static getListSectionHTML(
64
+ list: string[],
65
+ header: string,
66
+ className?: string,
67
+ ): string;
68
+ static getMarkdownSection(filePath: string): string;
69
+ }
70
+
71
+ export class Jekyll {
72
+ static include(fileName: string): string;
73
+ }
74
+
41
75
  export class Program {
42
76
  static getIncludeList(dataDir: string): string[];
43
77
  static getProgram(): Command;
@@ -55,3 +89,18 @@ export class Taxa {
55
89
  getTaxon(name: string): Taxon;
56
90
  getTaxonList(): Taxon[];
57
91
  }
92
+
93
+ export class Taxon {
94
+ getBaseFileName(): string;
95
+ getCalfloraID(): string;
96
+ getCalfloraTaxonLink(): string;
97
+ getCommonNames(): string[];
98
+ getFamily(): Family;
99
+ getGenus(): Genus;
100
+ getGenusName(): string;
101
+ getINatID(): string;
102
+ getINatTaxonLink(): string;
103
+ getName(): string;
104
+ getRPITaxonLink(): string;
105
+ getSynonyms(): string[];
106
+ }
package/lib/index.js CHANGED
@@ -6,6 +6,7 @@ import { Exceptions } from "./exceptions.js";
6
6
  import { Families } from "./families.js";
7
7
  import { Files } from "./files.js";
8
8
  import { HTML } from "./html.js";
9
+ import { HTMLTaxon } from "./htmltaxon.js";
9
10
  import { Jekyll } from "./jekyll.js";
10
11
  import { PlantBook } from "./ebook/plantbook.js";
11
12
  import { Program } from "./program.js";
@@ -21,6 +22,7 @@ export {
21
22
  Families,
22
23
  Files,
23
24
  HTML,
25
+ HTMLTaxon,
24
26
  Jekyll,
25
27
  PlantBook,
26
28
  Program,
@@ -0,0 +1,316 @@
1
+ import path from "node:path";
2
+ import { Files } from "../files.js";
3
+ import { CSV } from "../csv.js";
4
+ import { sleep } from "../util.js";
5
+ import { TaxaCSV } from "./taxacsv.js";
6
+
7
+ /**
8
+ * @typedef {{id:string,
9
+ * name:string,
10
+ * phylum:string,
11
+ * rank:string,
12
+ * scientificName:string,
13
+ * specificEpithet:string
14
+ * }} INatCSVData
15
+ */
16
+
17
+ export class INat {
18
+ /** @type {Object<string,InatTaxon>} */
19
+ static #taxa = {};
20
+
21
+ /**
22
+ * @param {string} toolsDataDir
23
+ * @param {string} dataDir
24
+ * @param {Taxa} taxa
25
+ * @param {import("../exceptions.js").Exceptions} exceptions
26
+ * @param {ErrorLog} errorLog
27
+ * @param {string} csvFileName
28
+ * @param {boolean} update
29
+ */
30
+ static async analyze(
31
+ toolsDataDir,
32
+ dataDir,
33
+ taxa,
34
+ exceptions,
35
+ errorLog,
36
+ csvFileName,
37
+ update,
38
+ ) {
39
+ const inatDataDir = toolsDataDir + "/inat";
40
+ const csvFilePath = inatDataDir + "/" + csvFileName;
41
+
42
+ // Create data directory if it's not there.
43
+ Files.mkdir(inatDataDir);
44
+
45
+ // Download the data file if it doesn't exist.
46
+ if (!Files.exists(csvFilePath)) {
47
+ const url =
48
+ "https://www.inaturalist.org/taxa/inaturalist-taxonomy.dwca.zip";
49
+ const zipFileName = path.basename(url);
50
+ const zipFilePath = inatDataDir + "/" + zipFileName;
51
+ console.log("retrieving iNaturalist species");
52
+ await Files.fetch(url, zipFilePath);
53
+ await Files.zipFileExtract(zipFilePath, "taxa.csv", csvFilePath);
54
+ }
55
+
56
+ console.log("loading iNaturalist species");
57
+ await CSV.parseStream(
58
+ inatDataDir,
59
+ csvFileName,
60
+ undefined,
61
+ undefined,
62
+ this.#checkTaxon,
63
+ );
64
+ console.log("iNat: " + Object.keys(this.#taxa).length + " taxa loaded");
65
+
66
+ const missingTaxa = [];
67
+
68
+ /**@type {Map<string,string>} */
69
+ const idsToUpdate = new Map();
70
+
71
+ for (const taxon of taxa.getTaxonList()) {
72
+ const name = taxon.getName();
73
+ if (name.includes(" unknown")) {
74
+ continue;
75
+ }
76
+ const iNatName = taxon.getINatName();
77
+ const iNatTaxon = this.#taxa[iNatName];
78
+ if (!iNatTaxon) {
79
+ if (!exceptions.hasException(name, "inat", "notintaxondata")) {
80
+ errorLog.log(name, "not found in " + csvFileName, iNatName);
81
+ }
82
+ missingTaxa.push({ name: name, iNatName: iNatName });
83
+ continue;
84
+ }
85
+ if (iNatTaxon.getID() !== taxon.getINatID()) {
86
+ errorLog.log(
87
+ name,
88
+ "iNat ID in " +
89
+ csvFileName +
90
+ " does not match ID in taxa.csv",
91
+ iNatTaxon.getID(),
92
+ taxon.getINatID(),
93
+ );
94
+ idsToUpdate.set(name, iNatTaxon.getID());
95
+ }
96
+ }
97
+
98
+ console.log("iNat: looking up missing names");
99
+ for (const data of missingTaxa) {
100
+ await this.#findCurrentName(
101
+ taxa,
102
+ exceptions,
103
+ errorLog,
104
+ data.name,
105
+ data.iNatName,
106
+ );
107
+ }
108
+
109
+ this.#checkExceptions(taxa, exceptions, errorLog);
110
+
111
+ if (update) {
112
+ updateTaxaCSV(dataDir, idsToUpdate);
113
+ }
114
+ }
115
+
116
+ /**
117
+ *
118
+ * @param {Taxa} taxa
119
+ * @param {import("../exceptions.js").Exceptions} exceptions
120
+ * @param {ErrorLog} errorLog
121
+ */
122
+ static #checkExceptions(taxa, exceptions, errorLog) {
123
+ // Check the iNat exceptions and make sure they still apply.
124
+ for (const [name, v] of exceptions.getExceptions()) {
125
+ const exceptions = v.inat;
126
+ if (!exceptions) {
127
+ continue;
128
+ }
129
+
130
+ // Make sure the taxon is still in our list.
131
+ const taxon = taxa.getTaxon(name);
132
+ if (!taxon) {
133
+ // Don't process global exceptions if taxon is not in local list.
134
+ if (taxa.isSubset() && !v.local) {
135
+ continue;
136
+ }
137
+ errorLog.log(name, "has iNat exceptions but not in taxa.tsv");
138
+ continue;
139
+ }
140
+
141
+ for (const [k] of Object.entries(exceptions)) {
142
+ const iNatData = INat.#taxa[name];
143
+ switch (k) {
144
+ case "notintaxondata":
145
+ if (iNatData) {
146
+ errorLog.log(
147
+ name,
148
+ "found in iNat data but has notintaxondata exception",
149
+ );
150
+ }
151
+ break;
152
+ default:
153
+ errorLog.log(name, "unrecognized iNat exception", k);
154
+ }
155
+ }
156
+ }
157
+ }
158
+
159
+ /**
160
+ * @param {INatCSVData} record
161
+ */
162
+ static #checkTaxon(record) {
163
+ if (record["phylum"] === "Tracheophyta" && record["specificEpithet"]) {
164
+ const name = record["scientificName"];
165
+ INat.#taxa[name] = new InatTaxon(record["id"]);
166
+ }
167
+ }
168
+
169
+ /**
170
+ *
171
+ * @param {Taxa} taxa
172
+ * @param {import("../exceptions.js").Exceptions} exceptions
173
+ * @param {ErrorLog} errorLog
174
+ * @param {string} name
175
+ * @param {string} iNatName
176
+ */
177
+ static async #findCurrentName(taxa, exceptions, errorLog, name, iNatName) {
178
+ /**
179
+ * @param {{matched_term:string,name:string,rank:string}[]} results
180
+ * @param {string} iNatName
181
+ */
182
+ function findMatchingResult(results, iNatName) {
183
+ if (results.length === 1) {
184
+ return results[0];
185
+ }
186
+ let match;
187
+ for (const result of results) {
188
+ if (result.matched_term === iNatName) {
189
+ if (match) {
190
+ errorLog.log(
191
+ iNatName,
192
+ "found more than one matched_term",
193
+ match.matched_term,
194
+ result.matched_term,
195
+ );
196
+ return;
197
+ }
198
+ match = result;
199
+ }
200
+ }
201
+ return match;
202
+ }
203
+
204
+ const url = new URL("https://api.inaturalist.org/v1/taxa");
205
+ url.searchParams.set("q", iNatName);
206
+
207
+ const response = await fetch(url);
208
+ const data = await response.json();
209
+
210
+ /** @type {{name:string,rank:string}|undefined} */
211
+ let result = findMatchingResult(data.results, iNatName);
212
+ if (result === undefined) {
213
+ const parts = iNatName.split(" ");
214
+ switch (parts.length) {
215
+ case 2:
216
+ // If it's "genus species", try "genus species species".
217
+ parts.push(parts[1]);
218
+ iNatName = parts.join(" ");
219
+ result = findMatchingResult(data.results, iNatName);
220
+ break;
221
+ case 3:
222
+ // If it's "genus species species", try "genus species".
223
+ if (parts[1] === parts[2]) {
224
+ iNatName = parts[0] + " " + parts[1];
225
+ result = findMatchingResult(data.results, iNatName);
226
+ }
227
+ break;
228
+ }
229
+ }
230
+
231
+ if (result === undefined) {
232
+ if (!exceptions.hasException(name, "inat", "notintaxondata")) {
233
+ errorLog.log(name, "iNat lookup found no results");
234
+ // Make sure this doesn't have an iNat ID.
235
+ const iNatID = taxa.getTaxon(name).getINatID();
236
+ if (iNatID) {
237
+ errorLog.log(
238
+ name,
239
+ "iNat lookup failed but has iNat ID",
240
+ iNatID,
241
+ );
242
+ }
243
+ }
244
+ } else {
245
+ errorLog.log(
246
+ name,
247
+ "found iNat synonym",
248
+ this.makeSynonymName(result, errorLog) + "," + name + ",INAT",
249
+ );
250
+ }
251
+
252
+ // Delay to throttle queries to iNat API.
253
+ await sleep(800);
254
+ }
255
+
256
+ /**
257
+ * @param {{name:string,rank:string}} iNatResult
258
+ * @param {ErrorLog} errorLog
259
+ */
260
+ static makeSynonymName(iNatResult, errorLog) {
261
+ const synParts = iNatResult.name.split(" ");
262
+ if (synParts.length === 3) {
263
+ switch (iNatResult.rank) {
264
+ case "subspecies":
265
+ case "variety":
266
+ synParts[3] = synParts[2];
267
+ synParts[2] =
268
+ iNatResult.rank === "variety" ? "var." : "subsp.";
269
+ break;
270
+ case "hybrid":
271
+ // Leave as is.
272
+ break;
273
+ default:
274
+ errorLog.log(
275
+ iNatResult.name,
276
+ "unrecognized iNat rank",
277
+ iNatResult.rank,
278
+ );
279
+ }
280
+ }
281
+ return synParts.join(" ");
282
+ }
283
+ }
284
+
285
+ class InatTaxon {
286
+ #id;
287
+
288
+ /**
289
+ * @param {string} id
290
+ */
291
+ constructor(id) {
292
+ this.#id = id;
293
+ }
294
+
295
+ getID() {
296
+ return this.#id;
297
+ }
298
+ }
299
+
300
+ /**
301
+ * @param {string} dataDir
302
+ * @param {Map<string,string>} idsToUpdate
303
+ */
304
+ function updateTaxaCSV(dataDir, idsToUpdate) {
305
+ const taxa = new TaxaCSV(dataDir);
306
+
307
+ for (const taxonData of taxa.getTaxa()) {
308
+ const id = idsToUpdate.get(taxonData.taxon_name);
309
+ if (!id) {
310
+ continue;
311
+ }
312
+ taxonData["inat id"] = id;
313
+ }
314
+
315
+ taxa.write();
316
+ }
@@ -4,8 +4,6 @@ import { GenericPage } from "../genericpage.js";
4
4
  import { ExternalSites } from "../externalsites.js";
5
5
  import { HTML } from "../html.js";
6
6
  import { HTMLTaxon } from "../htmltaxon.js";
7
- import { Markdown } from "../markdown.js";
8
- import { Config } from "../config.js";
9
7
 
10
8
  class PageTaxon extends GenericPage {
11
9
  #config;
@@ -82,24 +80,6 @@ class PageTaxon extends GenericPage {
82
80
  return links;
83
81
  }
84
82
 
85
- /**
86
- * @param {string[]} list
87
- * @param {string} header
88
- * @param {string} className
89
- */
90
- #getListSectionHTML(list, header, className) {
91
- let html = "";
92
- if (list.length > 0) {
93
- html += '<div class="section nobullet ' + className + '">';
94
- html += HTML.textElement("h2", header);
95
- html += "<ul>";
96
- html += HTML.arrayToLI(list);
97
- html += "</ul>";
98
- html += "</div>";
99
- }
100
- return html;
101
- }
102
-
103
83
  #getRarityInfo() {
104
84
  const cnpsRank = this.#taxon.getRPIRankAndThreat();
105
85
  if (!cnpsRank) {
@@ -185,28 +165,30 @@ class PageTaxon extends GenericPage {
185
165
  html += this.getMarkdown();
186
166
 
187
167
  html += '<div class="grid borders">';
188
- html += this.#getListSectionHTML(
168
+ html += HTMLTaxon.getListSectionHTML(
189
169
  this.#getInfoLinks(),
190
170
  "References",
191
171
  "info",
192
172
  );
193
- html += this.#getListSectionHTML(
173
+ html += HTMLTaxon.getListSectionHTML(
194
174
  this.#getObsLinks(),
195
175
  "Observations",
196
176
  "obs",
197
177
  );
198
- html += this.#getListSectionHTML(
178
+ html += HTMLTaxon.getListSectionHTML(
199
179
  this.#getRelatedTaxaLinks(),
200
180
  "Related Species",
201
181
  "rel-taxa",
202
182
  );
203
- html += this.#getListSectionHTML(
183
+ html += HTMLTaxon.getListSectionHTML(
204
184
  this.#getSynonyms(),
205
185
  "Synonyms",
206
186
  "synonyms",
207
187
  );
208
188
  html += "</div>";
209
189
 
190
+ html += HTMLTaxon.getFooterHTML(this.#taxon);
191
+
210
192
  const photos = this.#taxon.getPhotos();
211
193
  if (photos.length > 0) {
212
194
  let photosHtml = "";
@@ -235,16 +217,6 @@ class PageTaxon extends GenericPage {
235
217
  `;
236
218
  }
237
219
 
238
- const footerTextPath =
239
- Config.getPackageDir() +
240
- "/data/text/" +
241
- this.getBaseFileName() +
242
- ".footer.md";
243
- const footerMarkdown = Markdown.fileToHTML(footerTextPath);
244
- if (footerMarkdown) {
245
- html += HTML.wrap("div", footerMarkdown, "section");
246
- }
247
-
248
220
  this.writeFile(html);
249
221
  }
250
222
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ca-plant-list/ca-plant-list",
3
- "version": "0.4.13",
3
+ "version": "0.4.15",
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": {
@@ -8,6 +8,7 @@ import { Calflora } from "../lib/tools/calflora.js";
8
8
  import { Exceptions } from "../lib/exceptions.js";
9
9
  import { ErrorLog } from "../lib/errorlog.js";
10
10
  import { Calscape } from "../lib/tools/calscape.js";
11
+ import { INat } from "../lib/tools/inat.js";
11
12
 
12
13
  const TOOLS = {
13
14
  CALFLORA: "calflora",
@@ -72,13 +73,15 @@ async function build(program, options) {
72
73
  );
73
74
  break;
74
75
  case TOOLS.INAT:
75
- // await INat.analyze(
76
- // TOOLS_DATA_DIR,
77
- // taxa,
78
- // exceptions,
79
- // errorLog,
80
- // options.inTaxafile,
81
- // );
76
+ await INat.analyze(
77
+ TOOLS_DATA_DIR,
78
+ options.datadir,
79
+ taxa,
80
+ exceptions,
81
+ errorLog,
82
+ options.inTaxafile,
83
+ !!options.update,
84
+ );
82
85
  break;
83
86
  case TOOLS.JEPSON_EFLORA: {
84
87
  // const eflora = new JepsonEFlora(