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

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
@@ -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
@@ -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,6 @@
1
1
  import { DateUtils } from "./dateutils.js";
2
2
  import { HTML } from "./html.js";
3
+ import { Markdown } from "./markdown.js";
3
4
  import { RarePlants } from "./rareplants.js";
4
5
  import { TextUtils } from "./textutils.js";
5
6
 
@@ -136,6 +137,36 @@ class HTMLTaxon {
136
137
  );
137
138
  }
138
139
 
140
+ /**
141
+ * @param {string[]} list
142
+ * @param {string} header
143
+ * @param {string} [className]
144
+ */
145
+ static getListSectionHTML(list, header, className) {
146
+ let html = "";
147
+ if (list.length > 0) {
148
+ html += `<div class="section nobullet${className ? " " + className : ""}">`;
149
+ html += HTML.textElement("h2", header);
150
+ html += "<ul>";
151
+ html += HTML.arrayToLI(list);
152
+ html += "</ul>";
153
+ html += "</div>";
154
+ }
155
+ return html;
156
+ }
157
+
158
+ /**
159
+ * @param {string} filePath
160
+ * @returns {string}
161
+ */
162
+ static getMarkdownSection(filePath) {
163
+ const footerMarkdown = Markdown.fileToHTML(filePath);
164
+ if (footerMarkdown) {
165
+ return HTML.wrap("div", footerMarkdown, "section");
166
+ }
167
+ return "";
168
+ }
169
+
139
170
  /**
140
171
  * @param {Taxon[]} taxa
141
172
  * @param {TaxaCol[]} [columns]
package/lib/index.d.ts CHANGED
@@ -38,6 +38,39 @@ 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 getListSectionHTML(
63
+ list: string[],
64
+ header: string,
65
+ className?: string,
66
+ ): string;
67
+ static getMarkdownSection(filePath: string): string;
68
+ }
69
+
70
+ export class Jekyll {
71
+ static include(fileName: string): string;
72
+ }
73
+
41
74
  export class Program {
42
75
  static getIncludeList(dataDir: string): string[];
43
76
  static getProgram(): Command;
@@ -55,3 +88,18 @@ export class Taxa {
55
88
  getTaxon(name: string): Taxon;
56
89
  getTaxonList(): Taxon[];
57
90
  }
91
+
92
+ export class Taxon {
93
+ getBaseFileName(): string;
94
+ getCalfloraID(): string;
95
+ getCalfloraTaxonLink(): string;
96
+ getCommonNames(): string[];
97
+ getFamily(): Family;
98
+ getGenus(): Genus;
99
+ getGenusName(): string;
100
+ getINatID(): string;
101
+ getINatTaxonLink(): string;
102
+ getName(): string;
103
+ getRPITaxonLink(): string;
104
+ getSynonyms(): string[];
105
+ }
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,7 +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
7
  import { Config } from "../config.js";
9
8
 
10
9
  class PageTaxon extends GenericPage {
@@ -82,24 +81,6 @@ class PageTaxon extends GenericPage {
82
81
  return links;
83
82
  }
84
83
 
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
84
  #getRarityInfo() {
104
85
  const cnpsRank = this.#taxon.getRPIRankAndThreat();
105
86
  if (!cnpsRank) {
@@ -185,28 +166,35 @@ class PageTaxon extends GenericPage {
185
166
  html += this.getMarkdown();
186
167
 
187
168
  html += '<div class="grid borders">';
188
- html += this.#getListSectionHTML(
169
+ html += HTMLTaxon.getListSectionHTML(
189
170
  this.#getInfoLinks(),
190
171
  "References",
191
172
  "info",
192
173
  );
193
- html += this.#getListSectionHTML(
174
+ html += HTMLTaxon.getListSectionHTML(
194
175
  this.#getObsLinks(),
195
176
  "Observations",
196
177
  "obs",
197
178
  );
198
- html += this.#getListSectionHTML(
179
+ html += HTMLTaxon.getListSectionHTML(
199
180
  this.#getRelatedTaxaLinks(),
200
181
  "Related Species",
201
182
  "rel-taxa",
202
183
  );
203
- html += this.#getListSectionHTML(
184
+ html += HTMLTaxon.getListSectionHTML(
204
185
  this.#getSynonyms(),
205
186
  "Synonyms",
206
187
  "synonyms",
207
188
  );
208
189
  html += "</div>";
209
190
 
191
+ const footerTextPath =
192
+ Config.getPackageDir() +
193
+ "/data/text/" +
194
+ this.getBaseFileName() +
195
+ ".footer.md";
196
+ html += HTMLTaxon.getMarkdownSection(footerTextPath);
197
+
210
198
  const photos = this.#taxon.getPhotos();
211
199
  if (photos.length > 0) {
212
200
  let photosHtml = "";
@@ -235,16 +223,6 @@ class PageTaxon extends GenericPage {
235
223
  `;
236
224
  }
237
225
 
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
226
  this.writeFile(html);
249
227
  }
250
228
  }
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.14",
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(