@ca-plant-list/ca-plant-list 0.4.3 → 0.4.6
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/.prettierrc +1 -0
- package/README.md +27 -1
- package/data/config.json +5 -0
- package/data/inattaxonphotos.csv +6059 -0
- package/data/taxa.csv +1 -0
- package/data/text/Rosa-rubiginosa.footer.md +3 -0
- package/eslint.config.mjs +13 -0
- package/jekyll/assets/css/main.css +59 -59
- package/jekyll/index.md +3 -0
- package/lib/config.js +12 -4
- package/lib/ebook/ebook.js +178 -179
- package/lib/ebook/plantbook.js +147 -139
- package/lib/genericpage.js +3 -4
- package/lib/htmltaxon.js +118 -123
- package/lib/inat_photo.js +43 -0
- package/lib/index.d.ts +12 -9
- package/lib/markdown.js +4 -0
- package/lib/pagerenderer.js +220 -221
- package/lib/photo.js +44 -0
- package/lib/taxa.js +37 -3
- package/lib/taxon.js +13 -0
- package/lib/util.js +20 -0
- package/lib/web/pagetaxon.js +212 -181
- package/package.json +9 -6
- package/scripts/build-site.js +1 -1
- package/scripts/inattaxonphotos.js +106 -0
- package/types/classes.d.ts +55 -9
- package/.vscode/settings.json +0 -11
package/lib/taxon.js
CHANGED
@@ -34,6 +34,8 @@ class Taxon {
|
|
34
34
|
#rankGlobal;
|
35
35
|
/** @type {string[]} */
|
36
36
|
#synonyms = [];
|
37
|
+
/** @type {Photo[]} */
|
38
|
+
#photos = [];
|
37
39
|
|
38
40
|
/**
|
39
41
|
* @param {TaxonData} data
|
@@ -92,6 +94,17 @@ class Taxon {
|
|
92
94
|
}
|
93
95
|
}
|
94
96
|
|
97
|
+
/**
|
98
|
+
* @param {InatPhoto} photo
|
99
|
+
*/
|
100
|
+
addPhoto(photo) {
|
101
|
+
this.#photos = this.#photos.concat( [photo] );
|
102
|
+
}
|
103
|
+
|
104
|
+
getPhotos() {
|
105
|
+
return this.#photos;
|
106
|
+
}
|
107
|
+
|
95
108
|
getBaseFileName() {
|
96
109
|
// Convert spaces to "-" and remove ".".
|
97
110
|
return this.#name.replaceAll(" ", "-").replaceAll(".", "");
|
package/lib/util.js
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
/**
|
2
|
+
* Break an array into chunks of a desired size
|
3
|
+
* https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore?tab=readme-ov-file#_chunk
|
4
|
+
* @param {any[]} input
|
5
|
+
* @param {number} size
|
6
|
+
*/
|
7
|
+
export function chunk( input, size ) {
|
8
|
+
return input.reduce((arr, item, idx) => {
|
9
|
+
return idx % size === 0
|
10
|
+
? [...arr, [item]]
|
11
|
+
: [...arr.slice(0, -1), [...arr.slice(-1)[0], item]];
|
12
|
+
}, []);
|
13
|
+
}
|
14
|
+
|
15
|
+
/**
|
16
|
+
* @param {number} time
|
17
|
+
*/
|
18
|
+
export async function sleep( time ) {
|
19
|
+
return new Promise( resolve => setTimeout( resolve, time ) );
|
20
|
+
}
|
package/lib/web/pagetaxon.js
CHANGED
@@ -4,205 +4,236 @@ 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";
|
7
9
|
|
8
10
|
class PageTaxon extends GenericPage {
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
11
|
+
#config;
|
12
|
+
#taxon;
|
13
|
+
|
14
|
+
/**
|
15
|
+
* @param {string} outputDir
|
16
|
+
* @param {Config} config
|
17
|
+
* @param {Taxon} taxon
|
18
|
+
*/
|
19
|
+
constructor(outputDir, config, taxon) {
|
20
|
+
super(outputDir, taxon.getName(), taxon.getBaseFileName());
|
21
|
+
this.#config = config;
|
22
|
+
this.#taxon = taxon;
|
23
|
+
}
|
24
|
+
|
25
|
+
#getInfoLinks() {
|
26
|
+
const links = [];
|
27
|
+
const jepsonID = this.#taxon.getJepsonID();
|
28
|
+
if (jepsonID) {
|
29
|
+
links.push(Jepson.getEFloraLink(jepsonID));
|
21
30
|
}
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
const jepsonID = this.#taxon.getJepsonID();
|
26
|
-
if (jepsonID) {
|
27
|
-
links.push(Jepson.getEFloraLink(jepsonID));
|
28
|
-
}
|
29
|
-
const cfLink = this.#taxon.getCalfloraTaxonLink();
|
30
|
-
if (cfLink) {
|
31
|
-
links.push(cfLink);
|
32
|
-
}
|
33
|
-
const iNatLink = this.#taxon.getINatTaxonLink();
|
34
|
-
if (iNatLink) {
|
35
|
-
links.push(iNatLink);
|
36
|
-
}
|
37
|
-
const rpiLink = this.#taxon.getRPITaxonLink();
|
38
|
-
if (rpiLink) {
|
39
|
-
links.push(rpiLink);
|
40
|
-
}
|
41
|
-
return links;
|
31
|
+
const cfLink = this.#taxon.getCalfloraTaxonLink();
|
32
|
+
if (cfLink) {
|
33
|
+
links.push(cfLink);
|
42
34
|
}
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
links.push(
|
47
|
-
HTML.getLink(
|
48
|
-
"https://www.calflora.org/entry/observ.html?track=m#srch=t&grezc=5&cols=b&lpcli=t&cc=" +
|
49
|
-
this.#config.getCountyCodes().join("!") +
|
50
|
-
"&incobs=f&taxon=" +
|
51
|
-
this.#taxon.getCalfloraName().replaceAll(" ", "+"),
|
52
|
-
"Calflora",
|
53
|
-
{},
|
54
|
-
true
|
55
|
-
)
|
56
|
-
);
|
57
|
-
const iNatID = this.#taxon.getINatID();
|
58
|
-
if (iNatID) {
|
59
|
-
links.push(
|
60
|
-
HTML.getLink(
|
61
|
-
ExternalSites.getInatObsLink({
|
62
|
-
project_id: this.#config.getConfigValue(
|
63
|
-
"inat",
|
64
|
-
"project_id"
|
65
|
-
),
|
66
|
-
subview: "map",
|
67
|
-
taxon_id: iNatID,
|
68
|
-
}),
|
69
|
-
"iNaturalist",
|
70
|
-
{},
|
71
|
-
true
|
72
|
-
)
|
73
|
-
);
|
74
|
-
}
|
75
|
-
|
76
|
-
return links;
|
35
|
+
const iNatLink = this.#taxon.getINatTaxonLink();
|
36
|
+
if (iNatLink) {
|
37
|
+
links.push(iNatLink);
|
77
38
|
}
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
39
|
+
const rpiLink = this.#taxon.getRPITaxonLink();
|
40
|
+
if (rpiLink) {
|
41
|
+
links.push(rpiLink);
|
42
|
+
}
|
43
|
+
return links;
|
44
|
+
}
|
45
|
+
|
46
|
+
#getObsLinks() {
|
47
|
+
const links = [];
|
48
|
+
links.push(
|
49
|
+
HTML.getLink(
|
50
|
+
"https://www.calflora.org/entry/observ.html?track=m#srch=t&grezc=5&cols=b&lpcli=t&cc=" +
|
51
|
+
this.#config.getCountyCodes().join("!") +
|
52
|
+
"&incobs=f&taxon=" +
|
53
|
+
this.#taxon.getCalfloraName().replaceAll(" ", "+"),
|
54
|
+
"Calflora",
|
55
|
+
{},
|
56
|
+
true
|
57
|
+
)
|
58
|
+
);
|
59
|
+
const iNatID = this.#taxon.getINatID();
|
60
|
+
if (iNatID) {
|
61
|
+
const projectId = links.push(
|
62
|
+
HTML.getLink(
|
63
|
+
ExternalSites.getInatObsLink({
|
64
|
+
project_id: this.#config.getConfigValue("inat", "project_id"),
|
65
|
+
subview: "map",
|
66
|
+
taxon_id: iNatID,
|
67
|
+
}),
|
68
|
+
"iNaturalist",
|
69
|
+
{},
|
70
|
+
true
|
71
|
+
)
|
72
|
+
);
|
95
73
|
}
|
96
74
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
);
|
118
|
-
}
|
75
|
+
return links;
|
76
|
+
}
|
77
|
+
|
78
|
+
/**
|
79
|
+
* @param {string[]} list
|
80
|
+
* @param {string} header
|
81
|
+
* @param {string} className
|
82
|
+
*/
|
83
|
+
#getListSectionHTML(list, header, className) {
|
84
|
+
let html = "";
|
85
|
+
if (list.length > 0) {
|
86
|
+
html += '<div class="section nobullet ' + className + '">';
|
87
|
+
html += HTML.textElement("h2", header);
|
88
|
+
html += "<ul>";
|
89
|
+
html += HTML.arrayToLI(list);
|
90
|
+
html += "</ul>";
|
91
|
+
html += "</div>";
|
92
|
+
}
|
93
|
+
return html;
|
94
|
+
}
|
119
95
|
|
120
|
-
|
121
|
-
|
122
|
-
|
96
|
+
#getRarityInfo() {
|
97
|
+
const cnpsRank = this.#taxon.getRPIRankAndThreat();
|
98
|
+
if (!cnpsRank) {
|
99
|
+
return "";
|
100
|
+
}
|
101
|
+
const ranks = [];
|
102
|
+
|
103
|
+
ranks.push(
|
104
|
+
HTML.textElement("span", "CNPS Rare Plant Rank:", {
|
105
|
+
class: "label",
|
106
|
+
}) + HTML.getToolTip(cnpsRank, this.#taxon.getRPIRankAndThreatTooltip())
|
107
|
+
);
|
108
|
+
if (this.#taxon.getCESA()) {
|
109
|
+
ranks.push(
|
110
|
+
HTML.textElement("span", "CESA:", { class: "label" }) +
|
111
|
+
RarePlants.getCESADescription(this.#taxon.getCESA())
|
112
|
+
);
|
123
113
|
}
|
124
114
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
115
|
+
return HTML.wrap("div", "<ul>" + HTML.arrayToLI(ranks) + "</ul>", {
|
116
|
+
class: "section",
|
117
|
+
});
|
118
|
+
}
|
119
|
+
|
120
|
+
#getRelatedTaxaLinks() {
|
121
|
+
const links = [];
|
122
|
+
const genus = this.#taxon.getGenus();
|
123
|
+
if (genus) {
|
124
|
+
const taxa = genus.getTaxa();
|
125
|
+
if (taxa.length > 1) {
|
126
|
+
for (const taxon of taxa) {
|
127
|
+
links.push(
|
128
|
+
taxon.getHTMLLink(taxon.getName() !== this.#taxon.getName())
|
129
|
+
);
|
139
130
|
}
|
140
|
-
|
131
|
+
}
|
141
132
|
}
|
133
|
+
return links;
|
134
|
+
}
|
142
135
|
|
143
|
-
|
144
|
-
|
145
|
-
|
136
|
+
#getSynonyms() {
|
137
|
+
return this.#taxon.getSynonyms();
|
138
|
+
}
|
146
139
|
|
147
|
-
|
148
|
-
|
140
|
+
render() {
|
141
|
+
let html = this.getFrontMatter();
|
149
142
|
|
150
|
-
|
143
|
+
html += '<div class="wrapper">';
|
151
144
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
145
|
+
const cn = this.#taxon.getCommonNames();
|
146
|
+
if (cn.length > 0) {
|
147
|
+
html += HTML.textElement("div", cn.join(", "), {
|
148
|
+
class: "section common-names",
|
149
|
+
});
|
150
|
+
}
|
158
151
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
152
|
+
html += HTML.textElement(
|
153
|
+
"div",
|
154
|
+
this.#taxon.getStatusDescription(this.#config),
|
155
|
+
{ class: "section native-status" }
|
156
|
+
);
|
157
|
+
|
158
|
+
const family = this.#taxon.getFamily();
|
159
|
+
html += HTML.wrap(
|
160
|
+
"div",
|
161
|
+
HTML.textElement("span", "Family:", { class: "label" }) +
|
162
|
+
HTML.getLink("./" + family.getFileName(), family.getName()),
|
163
|
+
{ class: "section" }
|
164
|
+
);
|
165
|
+
|
166
|
+
html += this.#getRarityInfo();
|
167
|
+
|
168
|
+
html += "</div>";
|
169
|
+
|
170
|
+
html += HTMLTaxon.getFlowerInfo(this.#taxon, undefined, false);
|
171
|
+
|
172
|
+
html += this.getMarkdown();
|
173
|
+
|
174
|
+
html += '<div class="grid borders">';
|
175
|
+
html += this.#getListSectionHTML(
|
176
|
+
this.#getInfoLinks(),
|
177
|
+
"References",
|
178
|
+
"info"
|
179
|
+
);
|
180
|
+
html += this.#getListSectionHTML(
|
181
|
+
this.#getObsLinks(),
|
182
|
+
"Observations",
|
183
|
+
"obs"
|
184
|
+
);
|
185
|
+
html += this.#getListSectionHTML(
|
186
|
+
this.#getRelatedTaxaLinks(),
|
187
|
+
"Related Species",
|
188
|
+
"rel-taxa"
|
189
|
+
);
|
190
|
+
html += this.#getListSectionHTML(
|
191
|
+
this.#getSynonyms(),
|
192
|
+
"Synonyms",
|
193
|
+
"synonyms"
|
194
|
+
);
|
195
|
+
html += "</div>";
|
196
|
+
|
197
|
+
const photos = this.#taxon.getPhotos( );
|
198
|
+
if ( photos.length > 0 ) {
|
199
|
+
let photosHtml = "";
|
200
|
+
for ( const photo of photos ) {
|
201
|
+
photosHtml += `
|
202
|
+
<figure class="col">
|
203
|
+
<a href="${photo.getSourceUrl()}">
|
204
|
+
<img
|
205
|
+
class="img-fluid"
|
206
|
+
src="${photo.getUrl()}"
|
207
|
+
/>
|
208
|
+
</a>
|
209
|
+
<figcaption>
|
210
|
+
${photo.rights === "CC0" ? "By" : "(c)"}
|
211
|
+
${photo.rightsHolder}
|
212
|
+
${photo.rights && `(${photo.rights})`}
|
213
|
+
</figcaption>
|
214
|
+
</figure>
|
215
|
+
`;
|
216
|
+
}
|
217
|
+
html += `
|
218
|
+
<h2>Photos</h2>
|
219
|
+
<div class="row">
|
220
|
+
${photosHtml}
|
221
|
+
</div>
|
222
|
+
`;
|
205
223
|
}
|
224
|
+
|
225
|
+
const footerTextPath =
|
226
|
+
Config.getPackageDir() +
|
227
|
+
"/data/text/" +
|
228
|
+
this.getBaseFileName() +
|
229
|
+
".footer.md";
|
230
|
+
const footerMarkdown = Markdown.fileToHTML(footerTextPath);
|
231
|
+
if (footerMarkdown) {
|
232
|
+
html += HTML.wrap("div", footerMarkdown, "section");
|
233
|
+
}
|
234
|
+
|
235
|
+
this.writeFile(html);
|
236
|
+
}
|
206
237
|
}
|
207
238
|
|
208
239
|
export { PageTaxon };
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@ca-plant-list/ca-plant-list",
|
3
|
-
"version": "0.4.
|
3
|
+
"version": "0.4.6",
|
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": {
|
@@ -19,21 +19,24 @@
|
|
19
19
|
},
|
20
20
|
"dependencies": {
|
21
21
|
"archiver": "^5.3.1",
|
22
|
+
"cli-progress": "^3.12.0",
|
22
23
|
"commander": "^12.1.0",
|
23
24
|
"csv-parse": "^5.3.1",
|
25
|
+
"csv-stringify": "^6.5.1",
|
24
26
|
"image-size": "^1.1.1",
|
25
|
-
"markdown-it": "^
|
27
|
+
"markdown-it": "^14.1.0",
|
26
28
|
"sharp": "^0.32.1",
|
27
|
-
"svgo-ll": "^5.
|
29
|
+
"svgo-ll": "^5.6.0",
|
28
30
|
"unzipper": "^0.10.11"
|
29
31
|
},
|
30
32
|
"devDependencies": {
|
31
33
|
"@types/archiver": "^6.0.2",
|
34
|
+
"@types/cli-progress": "^3.11.6",
|
32
35
|
"@types/markdown-it": "^14.1.2",
|
33
|
-
"@types/node": "^22.
|
36
|
+
"@types/node": "^22.7.8",
|
34
37
|
"@types/unzipper": "^0.10.9",
|
35
38
|
"ajv-cli": "^5.0.0",
|
36
|
-
"eslint": "^
|
37
|
-
"typescript": "^5.
|
39
|
+
"eslint": "^9.13.0",
|
40
|
+
"typescript": "^5.6.3"
|
38
41
|
}
|
39
42
|
}
|
package/scripts/build-site.js
CHANGED
@@ -42,7 +42,7 @@ class JekyllRenderer {
|
|
42
42
|
addConfigFile(configFiles, this.#srcDir, "_config.yml");
|
43
43
|
addConfigFile(configFiles, this.#srcDir, "_config-local.yml");
|
44
44
|
addConfigFile(configFiles, ".", "_config-dev.yml");
|
45
|
-
options.push("--config",
|
45
|
+
options.push("--config", `"${configFiles.join()}"`);
|
46
46
|
|
47
47
|
const result = child_process.execSync(
|
48
48
|
"bundle exec jekyll build " + options.join(" ")
|
@@ -0,0 +1,106 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
|
3
|
+
import fs from "fs";
|
4
|
+
import cliProgress from "cli-progress";
|
5
|
+
import { stringify } from "csv-stringify";
|
6
|
+
import path from "path";
|
7
|
+
|
8
|
+
import { ErrorLog } from "../lib/errorlog.js";
|
9
|
+
import { Program } from "../lib/program.js";
|
10
|
+
import { Taxa } from "../lib/taxa.js";
|
11
|
+
import { chunk, sleep } from "../lib/util.js";
|
12
|
+
|
13
|
+
// While I'm guessing the products of this data will be non-commercial, it's
|
14
|
+
// not clear how they'll be licensed so the ShareAlike clause is out, and
|
15
|
+
// they'll probably be derivative works so the "No Derivatives" clause should
|
16
|
+
// be respected.
|
17
|
+
const ALLOWED_LICENSE_CODES = [
|
18
|
+
"cc0", "cc-by", "cc-by-nc"
|
19
|
+
];
|
20
|
+
|
21
|
+
/**
|
22
|
+
* @param {Taxon[]} taxa
|
23
|
+
* @return {Promise<InatApiTaxon[]>}
|
24
|
+
*/
|
25
|
+
async function fetchInatTaxa( taxa ) {
|
26
|
+
const inatTaxonIDs = taxa.map( taxon => taxon.getINatID( ) ).filter( Boolean );
|
27
|
+
const url = `https://api.inaturalist.org/v2/taxa/${inatTaxonIDs.join( "," )}?fields=(taxon_photos:(photo:(medium_url:!t,attribution:!t,license_code:!t)))`;
|
28
|
+
const resp = await fetch( url );
|
29
|
+
if (!resp.ok) {
|
30
|
+
const error = await resp.text();
|
31
|
+
throw new Error(`Failed to fetch taxa from iNat: ${error}`);
|
32
|
+
}
|
33
|
+
const json = await resp.json();
|
34
|
+
return json.results;
|
35
|
+
}
|
36
|
+
|
37
|
+
/**
|
38
|
+
* @param {CommandLineOptions} options
|
39
|
+
*/
|
40
|
+
async function getTaxonPhotos( options ) {
|
41
|
+
const errorLog = new ErrorLog(options.outputdir + "/errors.tsv");
|
42
|
+
const taxa = new Taxa(
|
43
|
+
Program.getIncludeList(options.datadir),
|
44
|
+
errorLog,
|
45
|
+
false
|
46
|
+
);
|
47
|
+
const targetTaxa = taxa.getTaxonList( );
|
48
|
+
|
49
|
+
const filename = path.join( "data", "inattaxonphotos.csv" );
|
50
|
+
const writableStream = fs.createWriteStream( filename );
|
51
|
+
const columns = [
|
52
|
+
"name",
|
53
|
+
"id",
|
54
|
+
"ext",
|
55
|
+
"licenseCode",
|
56
|
+
"attrName",
|
57
|
+
];
|
58
|
+
const stringifier = stringify( { header: true, columns: columns } );
|
59
|
+
stringifier.pipe(writableStream);
|
60
|
+
const prog = new cliProgress.SingleBar({
|
61
|
+
format: "Downloading [{bar}] {percentage}% | ETA: {eta}s | {value}/{total}",
|
62
|
+
etaBuffer: targetTaxa.length
|
63
|
+
});
|
64
|
+
prog.setMaxListeners( 100 );
|
65
|
+
prog.start( targetTaxa.length, 0 );
|
66
|
+
|
67
|
+
// Fetch endpoint can load multiple taxa, but it will created some long URLs so best to keep this smallish
|
68
|
+
for ( const batch of chunk( targetTaxa, 30 ) ) {
|
69
|
+
const inatTaxa = await fetchInatTaxa( batch );
|
70
|
+
for ( const taxon of batch ) {
|
71
|
+
prog.increment( );
|
72
|
+
const iNatTaxon = inatTaxa.find( it => it.id === Number( taxon.getINatID() ) );
|
73
|
+
if ( !iNatTaxon ) continue;
|
74
|
+
// Just get the CC-licensed ones, 5 per taxon should be fine (max is 20 on iNat). Whether or not
|
75
|
+
const taxonPhotos = iNatTaxon.taxon_photos
|
76
|
+
.filter( tp => ALLOWED_LICENSE_CODES.includes( tp.photo.license_code ) )
|
77
|
+
.slice( 0, 5 );
|
78
|
+
|
79
|
+
for ( const taxonPhoto of taxonPhotos ) {
|
80
|
+
const row = [
|
81
|
+
taxon.getName(),
|
82
|
+
taxonPhoto.photo.id,
|
83
|
+
taxonPhoto.photo.medium_url.split( "." ).at( -1 ),
|
84
|
+
// Need the license code to do attribution properly
|
85
|
+
taxonPhoto.photo.license_code,
|
86
|
+
// Photographers retain copyright for most CC licenses,
|
87
|
+
// except CC0, so attribution is a bit different
|
88
|
+
(
|
89
|
+
taxonPhoto.photo.attribution.match( /\(c\) (.*?),/ )?.[1]
|
90
|
+
|| taxonPhoto.photo.attribution.match( /uploaded by (.*)/ )?.[1]
|
91
|
+
)
|
92
|
+
];
|
93
|
+
stringifier.write( row );
|
94
|
+
}
|
95
|
+
}
|
96
|
+
// iNat will throttle you if you make more than 1 request a second.
|
97
|
+
// See https://www.inaturalist.org/pages/api+recommended+practices
|
98
|
+
await sleep( 1_100 );
|
99
|
+
}
|
100
|
+
prog.stop();
|
101
|
+
}
|
102
|
+
|
103
|
+
const program = Program.getProgram();
|
104
|
+
program.action(getTaxonPhotos).description( "Write a CSV to datadir with iNaturalist taxon photos" );
|
105
|
+
|
106
|
+
await program.parseAsync();
|