@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.
- package/data/glossary/awn.md +1 -0
- package/data/glossary/floret.md +4 -0
- package/data/glossary/glume.md +1 -0
- package/data/glossary/lemma.md +5 -0
- package/data/glossary/palea.md +1 -0
- package/data/glossary/rachilla.md +1 -0
- package/data/glossary/spikelet.md +5 -0
- package/data/synonyms.csv +1 -1
- package/data/taxa.csv +6 -6
- package/data/text/Eriastrum-ertterae.footer.md +6 -0
- package/data/text/Malacothamnus-arcuatus-var-elmeri.footer.md +3 -0
- package/data/text/Malacothamnus-fremontii-var-fremontii.footer.md +3 -0
- package/data/text/Malacothamnus-hallii.footer.md +3 -0
- package/jekyll/assets/css/main.css +56 -56
- package/lib/html.js +2 -2
- package/lib/htmltaxon.js +45 -0
- package/lib/index.d.ts +49 -0
- package/lib/index.js +2 -0
- package/lib/tools/inat.js +316 -0
- package/lib/web/pagetaxon.js +6 -34
- package/package.json +1 -1
- package/scripts/cpl-tools.js +10 -7
@@ -0,0 +1 @@
|
|
1
|
+
A bristle-like structure projecting from a [lemma](./lemma.html) or [glume](./glume.html).
|
@@ -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
|
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
|
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,
|
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
|
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
|
@@ -1,152 +1,152 @@
|
|
1
1
|
/* Defaults */
|
2
2
|
|
3
3
|
:root {
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
11
|
+
--bs-nav-link-hover-color: rgba(255, 255, 255, .5);
|
12
12
|
}
|
13
13
|
|
14
14
|
.navbar {
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
24
|
+
margin-bottom: .5rem;
|
25
25
|
}
|
26
26
|
|
27
27
|
span.label {
|
28
|
-
|
29
|
-
|
28
|
+
font-weight: 600;
|
29
|
+
padding-right: .5rem;
|
30
30
|
}
|
31
31
|
|
32
32
|
td {
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
border: solid 1px;
|
34
|
+
padding: 0 .5rem;
|
35
|
+
vertical-align: top;
|
36
36
|
}
|
37
37
|
|
38
38
|
th {
|
39
|
-
|
39
|
+
vertical-align: bottom;
|
40
40
|
}
|
41
41
|
|
42
42
|
ul.listmenu {
|
43
|
-
|
44
|
-
|
43
|
+
display: flex;
|
44
|
+
padding: 0;
|
45
45
|
}
|
46
46
|
|
47
47
|
ul.listmenu li {
|
48
|
-
|
49
|
-
|
50
|
-
|
48
|
+
display: inline-block;
|
49
|
+
padding: 0;
|
50
|
+
padding-right: .25em;
|
51
51
|
}
|
52
52
|
|
53
53
|
ul.listmenu li::after {
|
54
|
-
|
54
|
+
content: " |";
|
55
55
|
}
|
56
56
|
|
57
57
|
ul.listmenu li:last-child:after {
|
58
|
-
|
58
|
+
content: "";
|
59
59
|
}
|
60
60
|
|
61
61
|
/* Utilities */
|
62
62
|
.right {
|
63
|
-
|
63
|
+
text-align: right;
|
64
64
|
}
|
65
65
|
|
66
66
|
/* Lists */
|
67
67
|
|
68
68
|
span.native {
|
69
|
-
|
69
|
+
font-weight: bold;
|
70
70
|
}
|
71
71
|
|
72
72
|
span.rare::before {
|
73
|
-
|
73
|
+
content: "\2606";
|
74
74
|
}
|
75
75
|
|
76
76
|
/* Glossary */
|
77
77
|
div.glossary img {
|
78
|
-
|
78
|
+
max-height: 300px;
|
79
79
|
}
|
80
80
|
|
81
81
|
/* Taxon Page */
|
82
82
|
|
83
83
|
.common-names {
|
84
|
-
|
85
|
-
|
84
|
+
font-size: larger;
|
85
|
+
font-weight: bold;
|
86
86
|
}
|
87
87
|
|
88
88
|
.native-status {
|
89
|
-
|
89
|
+
font-weight: 600;
|
90
90
|
}
|
91
91
|
|
92
92
|
div.grid {
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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
|
-
|
101
|
-
|
102
|
-
|
100
|
+
border: solid 1px;
|
101
|
+
border-radius: 3%;
|
102
|
+
padding: .5rem;
|
103
103
|
}
|
104
104
|
|
105
105
|
div.wrapper {
|
106
|
-
|
107
|
-
|
108
|
-
|
106
|
+
display: flex;
|
107
|
+
flex-wrap: wrap;
|
108
|
+
column-gap: 4rem;
|
109
109
|
}
|
110
110
|
|
111
111
|
div.section {
|
112
|
-
|
112
|
+
margin-bottom: .5rem;
|
113
113
|
}
|
114
114
|
|
115
115
|
div.section h2 {
|
116
|
-
|
116
|
+
font-size: 1.25rem;
|
117
117
|
}
|
118
118
|
|
119
119
|
div.section.nobullet ul {
|
120
|
-
|
121
|
-
|
120
|
+
margin: 0;
|
121
|
+
padding: 0;
|
122
122
|
}
|
123
123
|
|
124
124
|
div.section.nobullet li {
|
125
|
-
|
126
|
-
|
127
|
-
|
125
|
+
display: block;
|
126
|
+
padding-left: .5rem;
|
127
|
+
text-indent: -.5rem;
|
128
128
|
}
|
129
129
|
|
130
130
|
div.section ul.indent {
|
131
|
-
|
131
|
+
padding-left: 1rem;
|
132
132
|
}
|
133
133
|
|
134
134
|
img.flr-color {
|
135
|
-
|
136
|
-
|
137
|
-
|
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
|
-
|
142
|
+
font-weight: bold;
|
143
143
|
}
|
144
144
|
|
145
145
|
span.lcs {
|
146
|
-
|
146
|
+
margin-right: 1rem;
|
147
147
|
}
|
148
148
|
|
149
149
|
/* Forms */
|
150
150
|
input {
|
151
|
-
|
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
|
+
}
|
package/lib/web/pagetaxon.js
CHANGED
@@ -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 +=
|
168
|
+
html += HTMLTaxon.getListSectionHTML(
|
189
169
|
this.#getInfoLinks(),
|
190
170
|
"References",
|
191
171
|
"info",
|
192
172
|
);
|
193
|
-
html +=
|
173
|
+
html += HTMLTaxon.getListSectionHTML(
|
194
174
|
this.#getObsLinks(),
|
195
175
|
"Observations",
|
196
176
|
"obs",
|
197
177
|
);
|
198
|
-
html +=
|
178
|
+
html += HTMLTaxon.getListSectionHTML(
|
199
179
|
this.#getRelatedTaxaLinks(),
|
200
180
|
"Related Species",
|
201
181
|
"rel-taxa",
|
202
182
|
);
|
203
|
-
html +=
|
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
package/scripts/cpl-tools.js
CHANGED
@@ -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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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(
|