@ca-plant-list/ca-plant-list 0.4.18 → 0.4.20
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/genera.json +36 -32
- package/data/inattaxonphotos.csv +4 -0
- package/data/synonyms.csv +2 -0
- package/data/taxa.csv +6 -5
- package/data/text/Polypodium-calirhiza.md +1 -0
- package/data/text/Polypodium-scouleri.md +1 -0
- package/data/text/Rumex-conglomeratus.md +1 -0
- package/data/text/Rumex-obtusifolius.md +1 -0
- package/data/text/Rumex-pulcher.md +1 -0
- package/data/text/Rumex-salicifolius.md +1 -0
- package/lib/basepagerenderer.js +3 -3
- package/lib/ebook/glossarypages.js +3 -3
- package/lib/ebook/images.js +7 -7
- package/lib/ebook/pages/page_list_families.js +1 -1
- package/lib/ebook/pages/page_list_flower_color.js +2 -2
- package/lib/ebook/pages/page_list_flowers.js +8 -8
- package/lib/ebook/pages/page_list_species.js +1 -1
- package/lib/ebook/pages/taxonpage.js +2 -2
- package/lib/ebook/pages/tocpage.js +2 -2
- package/lib/ebook/plantbook.js +3 -3
- package/lib/externalsites.js +20 -15
- package/lib/families.js +14 -14
- package/lib/flowercolor.js +2 -2
- package/lib/genera.js +3 -3
- package/lib/htmltaxon.js +16 -7
- package/lib/index.d.ts +46 -1
- package/lib/pagerenderer.js +223 -220
- package/lib/photo.js +52 -31
- package/lib/plants/glossary.js +2 -4
- package/lib/program.js +10 -2
- package/lib/sitegenerator.js +13 -3
- package/lib/taxa.js +16 -12
- package/lib/taxon.js +12 -6
- package/lib/tools/calflora.js +41 -8
- package/lib/tools/calscape.js +4 -4
- package/lib/tools/inat.js +7 -7
- package/lib/tools/jepsoneflora.js +28 -4
- package/lib/tools/jepsonfamilies.js +102 -0
- package/lib/tools/rpi.js +8 -9
- package/lib/tools/supplementaltext.js +43 -0
- package/lib/tools/taxacsv.js +2 -2
- package/lib/utils/inat-tools.js +39 -2
- package/lib/web/glossarypages.js +6 -6
- package/lib/web/pagetaxon.js +4 -6
- package/package.json +10 -8
- package/scripts/cpl-photos.js +2 -2
- package/scripts/cpl-tools.js +11 -3
- package/scripts/inatobsphotos.js +10 -1
- package/scripts/inattaxonphotos.js +45 -43
- package/lib/inat_photo.js +0 -43
- package/types/classes.d.ts +0 -232
package/lib/tools/rpi.js
CHANGED
@@ -12,10 +12,10 @@ class RPI {
|
|
12
12
|
|
13
13
|
/**
|
14
14
|
* @param {string} toolsDataDir
|
15
|
-
* @param {Taxa} taxa
|
16
|
-
* @param {Config} config
|
15
|
+
* @param {import("../taxa.js").Taxa} taxa
|
16
|
+
* @param {import("../config.js").Config} config
|
17
17
|
* @param {import("../exceptions.js").Exceptions} exceptions
|
18
|
-
* @param {ErrorLog} errorLog
|
18
|
+
* @param {import("../errorlog.js").ErrorLog} errorLog
|
19
19
|
*/
|
20
20
|
static async analyze(toolsDataDir, taxa, config, exceptions, errorLog) {
|
21
21
|
/**
|
@@ -202,11 +202,10 @@ class RPI {
|
|
202
202
|
}
|
203
203
|
|
204
204
|
/**
|
205
|
-
*
|
206
|
-
* @param {
|
207
|
-
* @param {Config} config
|
205
|
+
* @param {import("../taxa.js").Taxa} taxa
|
206
|
+
* @param {import("../config.js").Config} config
|
208
207
|
* @param {import("../exceptions.js").Exceptions} exceptions
|
209
|
-
* @param {ErrorLog} errorLog
|
208
|
+
* @param {import("../errorlog.js").ErrorLog} errorLog
|
210
209
|
*/
|
211
210
|
static #checkExceptions(taxa, config, exceptions, errorLog) {
|
212
211
|
const countyCodes = config.getCountyCodes();
|
@@ -367,9 +366,9 @@ class RPI {
|
|
367
366
|
|
368
367
|
/**
|
369
368
|
* @param {string} toolsDataDir
|
370
|
-
* @param {Taxa} taxa
|
369
|
+
* @param {import("../taxa.js").Taxa} taxa
|
371
370
|
* @param {import("../exceptions.js").Exceptions} exceptions
|
372
|
-
* @param {ErrorLog} errorLog
|
371
|
+
* @param {import("../errorlog.js").ErrorLog} errorLog
|
373
372
|
*/
|
374
373
|
static async #scrape(toolsDataDir, taxa, exceptions, errorLog) {
|
375
374
|
const toolsDataPath = toolsDataDir + "/rpi";
|
@@ -0,0 +1,43 @@
|
|
1
|
+
import { Files } from "../files.js";
|
2
|
+
|
3
|
+
const VALID_EXTENSIONS = new Set(["md", "footer.md"]);
|
4
|
+
|
5
|
+
export class SupplementalText {
|
6
|
+
/**
|
7
|
+
*
|
8
|
+
* @param {import("../taxa.js").Taxa} taxa
|
9
|
+
* @param {import("../errorlog.js").ErrorLog} errorLog
|
10
|
+
*/
|
11
|
+
static analyze(taxa, errorLog) {
|
12
|
+
/**
|
13
|
+
* @param {string} fileName
|
14
|
+
*/
|
15
|
+
function fileNameToTaxonName(fileName) {
|
16
|
+
const parts = fileName.split(".");
|
17
|
+
const ext = parts.slice(1).join(".");
|
18
|
+
const taxonName = parts[0]
|
19
|
+
.replace("-", " ")
|
20
|
+
.replace("-var-", " var. ")
|
21
|
+
.replace("-subsp-", " subsp. ");
|
22
|
+
return { taxonName: taxonName, ext: ext };
|
23
|
+
}
|
24
|
+
|
25
|
+
const dirName = "data/text";
|
26
|
+
|
27
|
+
if (!Files.isDir(dirName)) {
|
28
|
+
return;
|
29
|
+
}
|
30
|
+
|
31
|
+
const entries = Files.getDirEntries(dirName);
|
32
|
+
for (const entry of entries) {
|
33
|
+
const parsed = fileNameToTaxonName(entry);
|
34
|
+
const taxon = taxa.getTaxon(parsed.taxonName);
|
35
|
+
if (!taxon) {
|
36
|
+
errorLog.log(dirName + "/" + entry, "not found in taxa.csv");
|
37
|
+
}
|
38
|
+
if (!VALID_EXTENSIONS.has(parsed.ext)) {
|
39
|
+
errorLog.log(dirName + "/" + entry, "has invalid extension");
|
40
|
+
}
|
41
|
+
}
|
42
|
+
}
|
43
|
+
}
|
package/lib/tools/taxacsv.js
CHANGED
@@ -4,7 +4,7 @@ import { CSV } from "../csv.js";
|
|
4
4
|
export class TaxaCSV {
|
5
5
|
#filePath;
|
6
6
|
#headers;
|
7
|
-
/** @type {TaxonData[]} */
|
7
|
+
/** @type {import("../index.js").TaxonData[]} */
|
8
8
|
#taxa;
|
9
9
|
|
10
10
|
/**
|
@@ -19,7 +19,7 @@ export class TaxaCSV {
|
|
19
19
|
}
|
20
20
|
|
21
21
|
/**
|
22
|
-
* @returns {TaxonData[]}
|
22
|
+
* @returns {import("../index.js").TaxonData[]}
|
23
23
|
*/
|
24
24
|
getTaxa() {
|
25
25
|
return this.#taxa;
|
package/lib/utils/inat-tools.js
CHANGED
@@ -1,10 +1,47 @@
|
|
1
1
|
import { ProgressMeter } from "../progressmeter.js";
|
2
2
|
import { chunk, sleep } from "../util.js";
|
3
3
|
|
4
|
+
/**
|
5
|
+
@typedef {"cc-by-nc-sa"
|
6
|
+
| "cc-by-nc"
|
7
|
+
| "cc-by-nc-nd"
|
8
|
+
| "cc-by"
|
9
|
+
| "cc-by-sa"
|
10
|
+
| "cc-by-nd"
|
11
|
+
| "pd"
|
12
|
+
| "gdfl"
|
13
|
+
| "cc0"} InatLicenseCode
|
14
|
+
@typedef {{
|
15
|
+
id: string;
|
16
|
+
ext: string;
|
17
|
+
licenseCode: string;
|
18
|
+
attrName: string | undefined;
|
19
|
+
}} InatPhotoInfo
|
20
|
+
@typedef {{
|
21
|
+
id: number;
|
22
|
+
attribution: string;
|
23
|
+
license_code: InatLicenseCode;
|
24
|
+
medium_url?: string;
|
25
|
+
url?: string;
|
26
|
+
}} InatApiPhoto
|
27
|
+
@typedef {{
|
28
|
+
id: number;
|
29
|
+
taxon_photos: {
|
30
|
+
photo: InatApiPhoto;
|
31
|
+
}[];
|
32
|
+
}} InatApiTaxon
|
33
|
+
@typedef {{
|
34
|
+
name: string;
|
35
|
+
id: number;
|
36
|
+
ext: string;
|
37
|
+
licenseCode: InatLicenseCode;
|
38
|
+
attrName: string;
|
39
|
+
}} InatCsvPhoto
|
40
|
+
*/
|
4
41
|
const ALLOWED_LICENSE_CODES = ["cc0", "cc-by", "cc-by-nc"];
|
5
42
|
|
6
43
|
/**
|
7
|
-
* @param {Taxon[]} taxa
|
44
|
+
* @param {import("../taxon.js").Taxon[]} taxa
|
8
45
|
* @return {Promise<InatApiTaxon[]>}
|
9
46
|
*/
|
10
47
|
async function fetchInatTaxa(taxa) {
|
@@ -20,7 +57,7 @@ async function fetchInatTaxa(taxa) {
|
|
20
57
|
}
|
21
58
|
|
22
59
|
/**
|
23
|
-
* @param {Taxon[]} taxaToUpdate
|
60
|
+
* @param {import("../taxon.js").Taxon[]} taxaToUpdate
|
24
61
|
* @returns {Promise<Map<string,InatPhotoInfo[]>>}
|
25
62
|
*/
|
26
63
|
export async function getTaxonPhotos(taxaToUpdate) {
|
package/lib/web/glossarypages.js
CHANGED
@@ -10,7 +10,7 @@ class GlossaryPages {
|
|
10
10
|
#glossary;
|
11
11
|
|
12
12
|
/**
|
13
|
-
* @param {SiteGenerator} siteGenerator
|
13
|
+
* @param {import("../sitegenerator.js").SiteGenerator} siteGenerator
|
14
14
|
*/
|
15
15
|
constructor(siteGenerator) {
|
16
16
|
this.#siteGenerator = siteGenerator;
|
@@ -18,7 +18,7 @@ class GlossaryPages {
|
|
18
18
|
}
|
19
19
|
|
20
20
|
/**
|
21
|
-
* @param {GlossaryEntry} entry
|
21
|
+
* @param {import("../plants/glossary.js").GlossaryEntry} entry
|
22
22
|
*/
|
23
23
|
#generateEntryPage(entry) {
|
24
24
|
const title = entry.getTermName();
|
@@ -29,7 +29,7 @@ class GlossaryPages {
|
|
29
29
|
this.#siteGenerator.writeTemplate(
|
30
30
|
html,
|
31
31
|
{ title: title },
|
32
|
-
Files.join(ENTRY_DIR, title + ".html")
|
32
|
+
Files.join(ENTRY_DIR, title + ".html"),
|
33
33
|
);
|
34
34
|
}
|
35
35
|
|
@@ -50,8 +50,8 @@ class GlossaryPages {
|
|
50
50
|
links.push(
|
51
51
|
HTML.getLink(
|
52
52
|
Files.join(ENTRY_DIR, entry.getHTMLFileName()),
|
53
|
-
entry.getTermName()
|
54
|
-
)
|
53
|
+
entry.getTermName(),
|
54
|
+
),
|
55
55
|
);
|
56
56
|
}
|
57
57
|
let html = HTML.wrap("h1", "Glossary");
|
@@ -59,7 +59,7 @@ class GlossaryPages {
|
|
59
59
|
this.#siteGenerator.writeTemplate(
|
60
60
|
html,
|
61
61
|
{ title: "Glossary" },
|
62
|
-
"glossary.html"
|
62
|
+
"glossary.html",
|
63
63
|
);
|
64
64
|
}
|
65
65
|
|
package/lib/web/pagetaxon.js
CHANGED
@@ -11,8 +11,8 @@ class PageTaxon extends GenericPage {
|
|
11
11
|
|
12
12
|
/**
|
13
13
|
* @param {string} outputDir
|
14
|
-
* @param {Config} config
|
15
|
-
* @param {Taxon} taxon
|
14
|
+
* @param {import("../config.js").Config} config
|
15
|
+
* @param {import("../taxon.js").Taxon} taxon
|
16
16
|
*/
|
17
17
|
constructor(outputDir, config, taxon) {
|
18
18
|
super(outputDir, taxon.getName(), taxon.getBaseFileName());
|
@@ -189,7 +189,7 @@ class PageTaxon extends GenericPage {
|
|
189
189
|
|
190
190
|
html += HTMLTaxon.getFooterHTML(this.#taxon);
|
191
191
|
|
192
|
-
const photos = this.#taxon.getPhotos().slice(
|
192
|
+
const photos = this.#taxon.getPhotos().slice(0, 5);
|
193
193
|
if (photos.length > 0) {
|
194
194
|
let photosHtml = "";
|
195
195
|
for (const photo of photos) {
|
@@ -202,9 +202,7 @@ class PageTaxon extends GenericPage {
|
|
202
202
|
/>
|
203
203
|
</a>
|
204
204
|
<figcaption>
|
205
|
-
${photo.
|
206
|
-
${photo.rightsHolder}
|
207
|
-
${photo.rights && `(${photo.rights})`}
|
205
|
+
${photo.getAttribution()}
|
208
206
|
</figcaption>
|
209
207
|
</figure>
|
210
208
|
`;
|
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.20",
|
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": {
|
@@ -14,16 +14,16 @@
|
|
14
14
|
"ebook",
|
15
15
|
"jekyll",
|
16
16
|
"lib",
|
17
|
-
"scripts"
|
18
|
-
"types"
|
17
|
+
"scripts"
|
19
18
|
],
|
20
19
|
"exports": {
|
21
20
|
".": "./lib/index.js"
|
22
21
|
},
|
23
22
|
"types": "./lib/index.d.ts",
|
24
23
|
"scripts": {
|
25
|
-
"check": "npm run eslint && npm run tsc",
|
24
|
+
"check": "npm run eslint && npm run tsc && npm run jest",
|
26
25
|
"eslint": "npx eslint",
|
26
|
+
"jest": "node --experimental-vm-modules node_modules/jest/bin/jest.js tests",
|
27
27
|
"prettier": "npx prettier -l .",
|
28
28
|
"tsc": "npx tsc"
|
29
29
|
},
|
@@ -35,7 +35,7 @@
|
|
35
35
|
"inatobsphotos": "scripts/inatobsphotos.js"
|
36
36
|
},
|
37
37
|
"dependencies": {
|
38
|
-
"@htmltools/scrape": "^0.1.
|
38
|
+
"@htmltools/scrape": "^0.1.1",
|
39
39
|
"archiver": "^5.3.1",
|
40
40
|
"cli-progress": "^3.12.0",
|
41
41
|
"commander": "^12.1.0",
|
@@ -51,11 +51,13 @@
|
|
51
51
|
"devDependencies": {
|
52
52
|
"@types/archiver": "^6.0.2",
|
53
53
|
"@types/cli-progress": "^3.11.6",
|
54
|
+
"@types/jest": "^29.5.14",
|
54
55
|
"@types/markdown-it": "^14.1.2",
|
55
|
-
"@types/node": "^22.10.
|
56
|
+
"@types/node": "^22.10.7",
|
56
57
|
"@types/unzipper": "^0.10.9",
|
57
|
-
"eslint": "^9.
|
58
|
+
"eslint": "^9.18.0",
|
59
|
+
"jest": "^29.7.0",
|
58
60
|
"prettier": "^3.4.2",
|
59
|
-
"typescript": "^5.7.
|
61
|
+
"typescript": "^5.7.3"
|
60
62
|
}
|
61
63
|
}
|
package/scripts/cpl-photos.js
CHANGED
@@ -129,7 +129,7 @@ async function getTaxa(options) {
|
|
129
129
|
}
|
130
130
|
|
131
131
|
/**
|
132
|
-
* @returns {Map<string,InatPhotoInfo[]>}
|
132
|
+
* @returns {Map<string,import("../lib/utils/inat-tools.js").InatPhotoInfo[]>}
|
133
133
|
*/
|
134
134
|
function readPhotos() {
|
135
135
|
const photosFileName = `./data/${PHOTO_FILE_NAME}`;
|
@@ -140,7 +140,7 @@ function readPhotos() {
|
|
140
140
|
/** @type {Map<string,{id:string,ext:string,licenseCode:string,attrName:string}[]>} */
|
141
141
|
const taxonPhotos = new Map();
|
142
142
|
|
143
|
-
/** @type {InatCsvPhoto[]} */
|
143
|
+
/** @type {import("../lib/utils/inat-tools.js").InatCsvPhoto[]} */
|
144
144
|
// @ts-ignore
|
145
145
|
const csvPhotos = CSV.readFile(photosFileName);
|
146
146
|
for (const csvPhoto of csvPhotos) {
|
package/scripts/cpl-tools.js
CHANGED
@@ -11,6 +11,8 @@ import { JepsonEFlora } from "../lib/tools/jepsoneflora.js";
|
|
11
11
|
import { RPI } from "../lib/tools/rpi.js";
|
12
12
|
import { Config } from "../lib/config.js";
|
13
13
|
import { Taxa } from "../lib/taxa.js";
|
14
|
+
import { SupplementalText } from "../lib/tools/supplementaltext.js";
|
15
|
+
import { JepsonFamilies } from "../lib/tools/jepsonfamilies.js";
|
14
16
|
|
15
17
|
const TOOLS = {
|
16
18
|
CALFLORA: "calflora",
|
@@ -58,9 +60,11 @@ async function build(program, options) {
|
|
58
60
|
case TOOLS.CALFLORA:
|
59
61
|
await Calflora.analyze(
|
60
62
|
TOOLS_DATA_DIR,
|
63
|
+
options.datadir,
|
61
64
|
taxa,
|
62
65
|
exceptions,
|
63
66
|
errorLog,
|
67
|
+
!!options.update,
|
64
68
|
);
|
65
69
|
break;
|
66
70
|
case TOOLS.CALSCAPE:
|
@@ -86,11 +90,15 @@ async function build(program, options) {
|
|
86
90
|
break;
|
87
91
|
case TOOLS.JEPSON_EFLORA: {
|
88
92
|
const eflora = new JepsonEFlora(TOOLS_DATA_DIR, taxa, errorLog);
|
89
|
-
await eflora.analyze(
|
93
|
+
await eflora.analyze(
|
94
|
+
options.datadir,
|
95
|
+
exceptions,
|
96
|
+
!!options.update,
|
97
|
+
);
|
90
98
|
break;
|
91
99
|
}
|
92
100
|
case TOOLS.JEPSON_FAM:
|
93
|
-
|
101
|
+
await JepsonFamilies.build(TOOLS_DATA_DIR, "./data");
|
94
102
|
break;
|
95
103
|
case TOOLS.RPI:
|
96
104
|
await RPI.analyze(
|
@@ -102,7 +110,7 @@ async function build(program, options) {
|
|
102
110
|
);
|
103
111
|
break;
|
104
112
|
case TOOLS.TEXT:
|
105
|
-
|
113
|
+
SupplementalText.analyze(taxa, errorLog);
|
106
114
|
break;
|
107
115
|
default:
|
108
116
|
console.log("unrecognized tool: " + tool);
|
package/scripts/inatobsphotos.js
CHANGED
@@ -9,6 +9,15 @@ import { Program } from "../lib/program.js";
|
|
9
9
|
import { Taxa } from "../lib/taxa.js";
|
10
10
|
import { sleep } from "../lib/util.js";
|
11
11
|
|
12
|
+
/**
|
13
|
+
* @typedef {{
|
14
|
+
observation_photos: {
|
15
|
+
photo: import("../lib/utils/inat-tools.js").InatApiPhoto;
|
16
|
+
}[];
|
17
|
+
}} InatApiObservation
|
18
|
+
@typedef {import("../lib/program.js").CommandLineOptions &{filename?:string,inatObsQuery?:string}} InatObsPhotosCommandLineOptions
|
19
|
+
*/
|
20
|
+
|
12
21
|
// While I'm guessing the products of this data will be non-commercial, it's
|
13
22
|
// not clear how they'll be licensed so the ShareAlike clause is out, and
|
14
23
|
// they'll probably be derivative works so the "No Derivatives" clause should
|
@@ -18,7 +27,7 @@ const ALLOWED_LICENSE_CODES = ["cc0", "cc-by", "cc-by-nc"];
|
|
18
27
|
const DEFAULT_FILENAME = "inatobsphotos.csv";
|
19
28
|
|
20
29
|
/**
|
21
|
-
* @param {Taxon} taxon
|
30
|
+
* @param {import("../lib/taxon.js").Taxon} taxon
|
22
31
|
* @param {InatObsPhotosCommandLineOptions} options
|
23
32
|
* @return {Promise<InatApiObservation[]>}
|
24
33
|
*/
|
@@ -10,24 +10,26 @@ import { Program } from "../lib/program.js";
|
|
10
10
|
import { Taxa } from "../lib/taxa.js";
|
11
11
|
import { chunk, sleep } from "../lib/util.js";
|
12
12
|
|
13
|
+
/**
|
14
|
+
* @typedef {import("../lib/program.js").CommandLineOptions & {filename?:string}} InatTaxonPhotosCommandLineOptions
|
15
|
+
*/
|
16
|
+
|
13
17
|
// While I'm guessing the products of this data will be non-commercial, it's
|
14
18
|
// not clear how they'll be licensed so the ShareAlike clause is out, and
|
15
19
|
// they'll probably be derivative works so the "No Derivatives" clause should
|
16
20
|
// be respected.
|
17
|
-
const ALLOWED_LICENSE_CODES = [
|
18
|
-
"cc0", "cc-by", "cc-by-nc"
|
19
|
-
];
|
21
|
+
const ALLOWED_LICENSE_CODES = ["cc0", "cc-by", "cc-by-nc"];
|
20
22
|
|
21
23
|
const DEFAULT_FILENAME = "inattaxonphotos.csv";
|
22
24
|
|
23
25
|
/**
|
24
|
-
* @param {Taxon[]} taxa
|
25
|
-
* @return {Promise<InatApiTaxon[]>}
|
26
|
+
* @param {import("../lib/taxon.js").Taxon[]} taxa
|
27
|
+
* @return {Promise<import("../lib/utils/inat-tools.js").InatApiTaxon[]>}
|
26
28
|
*/
|
27
|
-
async function fetchInatTaxa(
|
28
|
-
const inatTaxonIDs = taxa.map(
|
29
|
-
const url = `https://api.inaturalist.org/v2/taxa/${inatTaxonIDs.join(
|
30
|
-
const resp = await fetch(
|
29
|
+
async function fetchInatTaxa(taxa) {
|
30
|
+
const inatTaxonIDs = taxa.map((taxon) => taxon.getINatID()).filter(Boolean);
|
31
|
+
const url = `https://api.inaturalist.org/v2/taxa/${inatTaxonIDs.join(",")}?fields=(taxon_photos:(photo:(medium_url:!t,attribution:!t,license_code:!t)))`;
|
32
|
+
const resp = await fetch(url);
|
31
33
|
if (!resp.ok) {
|
32
34
|
const error = await resp.text();
|
33
35
|
throw new Error(`Failed to fetch taxa from iNat: ${error}`);
|
@@ -39,74 +41,74 @@ async function fetchInatTaxa( taxa ) {
|
|
39
41
|
/**
|
40
42
|
* @param {InatTaxonPhotosCommandLineOptions} options
|
41
43
|
*/
|
42
|
-
async function getTaxonPhotos(
|
44
|
+
async function getTaxonPhotos(options) {
|
43
45
|
const errorLog = new ErrorLog(options.outputdir + "/errors.tsv");
|
44
46
|
const taxa = new Taxa(
|
45
47
|
Program.getIncludeList(options.datadir),
|
46
48
|
errorLog,
|
47
|
-
false
|
49
|
+
false,
|
48
50
|
);
|
49
|
-
const targetTaxa = taxa.getTaxonList(
|
51
|
+
const targetTaxa = taxa.getTaxonList();
|
50
52
|
|
51
53
|
const filename = path.join("data", options.filename || DEFAULT_FILENAME);
|
52
|
-
const writableStream = fs.createWriteStream(
|
53
|
-
const columns = [
|
54
|
-
|
55
|
-
"id",
|
56
|
-
"ext",
|
57
|
-
"licenseCode",
|
58
|
-
"attrName",
|
59
|
-
];
|
60
|
-
const stringifier = stringify( { header: true, columns: columns } );
|
54
|
+
const writableStream = fs.createWriteStream(filename);
|
55
|
+
const columns = ["name", "id", "ext", "licenseCode", "attrName"];
|
56
|
+
const stringifier = stringify({ header: true, columns: columns });
|
61
57
|
stringifier.pipe(writableStream);
|
62
58
|
const prog = new cliProgress.SingleBar({
|
63
59
|
format: "Downloading [{bar}] {percentage}% | ETA: {eta}s | {value}/{total}",
|
64
|
-
etaBuffer: targetTaxa.length
|
60
|
+
etaBuffer: targetTaxa.length,
|
65
61
|
});
|
66
|
-
prog.setMaxListeners(
|
67
|
-
prog.start(
|
62
|
+
prog.setMaxListeners(100);
|
63
|
+
prog.start(targetTaxa.length, 0);
|
68
64
|
|
69
65
|
// Fetch endpoint can load multiple taxa, but it will created some long URLs so best to keep this smallish
|
70
|
-
for (
|
71
|
-
const inatTaxa = await fetchInatTaxa(
|
72
|
-
for (
|
73
|
-
prog.increment(
|
74
|
-
const iNatTaxon = inatTaxa.find(
|
75
|
-
|
76
|
-
|
66
|
+
for (const batch of chunk(targetTaxa, 30)) {
|
67
|
+
const inatTaxa = await fetchInatTaxa(batch);
|
68
|
+
for (const taxon of batch) {
|
69
|
+
prog.increment();
|
70
|
+
const iNatTaxon = inatTaxa.find(
|
71
|
+
(it) => it.id === Number(taxon.getINatID()),
|
72
|
+
);
|
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
|
77
75
|
const taxonPhotos = iNatTaxon.taxon_photos
|
78
|
-
.filter(
|
79
|
-
|
76
|
+
.filter((tp) =>
|
77
|
+
ALLOWED_LICENSE_CODES.includes(tp.photo.license_code),
|
78
|
+
)
|
79
|
+
.slice(0, 5);
|
80
80
|
|
81
|
-
for (
|
81
|
+
for (const taxonPhoto of taxonPhotos) {
|
82
82
|
const row = [
|
83
83
|
taxon.getName(),
|
84
84
|
taxonPhoto.photo.id,
|
85
|
-
String(
|
85
|
+
String(taxonPhoto.photo.medium_url).split(".").at(-1),
|
86
86
|
// Need the license code to do attribution properly
|
87
87
|
taxonPhoto.photo.license_code,
|
88
88
|
// Photographers retain copyright for most CC licenses,
|
89
89
|
// except CC0, so attribution is a bit different
|
90
|
-
(
|
91
|
-
taxonPhoto.photo.attribution.match(
|
92
|
-
|
93
|
-
|
90
|
+
taxonPhoto.photo.attribution.match(/\(c\) (.*?),/)?.[1] ||
|
91
|
+
taxonPhoto.photo.attribution.match(
|
92
|
+
/uploaded by (.*)/,
|
93
|
+
)?.[1],
|
94
94
|
];
|
95
|
-
stringifier.write(
|
95
|
+
stringifier.write(row);
|
96
96
|
}
|
97
97
|
}
|
98
98
|
// iNat will throttle you if you make more than 1 request a second.
|
99
99
|
// See https://www.inaturalist.org/pages/api+recommended+practices
|
100
|
-
await sleep(
|
100
|
+
await sleep(1_100);
|
101
101
|
}
|
102
102
|
prog.stop();
|
103
103
|
}
|
104
104
|
|
105
105
|
const program = Program.getProgram();
|
106
|
-
program
|
106
|
+
program
|
107
|
+
.action(getTaxonPhotos)
|
108
|
+
.description("Write a CSV to datadir with iNaturalist taxon photos")
|
107
109
|
.option(
|
108
110
|
"-fn, --filename <filename>",
|
109
|
-
"Name of file to write to the data dir"
|
111
|
+
"Name of file to write to the data dir",
|
110
112
|
);
|
111
113
|
|
112
114
|
await program.parseAsync();
|
package/lib/inat_photo.js
DELETED
@@ -1,43 +0,0 @@
|
|
1
|
-
import { CC0, CC_BY, CC_BY_NC, COPYRIGHT, Photo } from "./photo.js";
|
2
|
-
|
3
|
-
class InatPhoto extends Photo {
|
4
|
-
/** @type {number} */
|
5
|
-
inatPhotoId;
|
6
|
-
/** @type {string} */
|
7
|
-
ext;
|
8
|
-
|
9
|
-
/**
|
10
|
-
* @param {number} id
|
11
|
-
* @param {string} ext
|
12
|
-
* @param {InatLicenseCode} licenseCode
|
13
|
-
* @param {string} attrName
|
14
|
-
*/
|
15
|
-
constructor(id, ext, licenseCode, attrName) {
|
16
|
-
/** @type {typeof COPYRIGHT | typeof CC_BY | typeof CC_BY_NC | typeof CC0} */
|
17
|
-
let rights = COPYRIGHT;
|
18
|
-
if (licenseCode === "cc0") rights = CC0;
|
19
|
-
else if (licenseCode === "cc-by") rights = CC_BY;
|
20
|
-
else if (licenseCode === "cc-by-nc") rights = CC_BY_NC;
|
21
|
-
super(null, attrName, rights);
|
22
|
-
this.inatPhotoId = id;
|
23
|
-
this.ext = ext;
|
24
|
-
}
|
25
|
-
|
26
|
-
getExt() {
|
27
|
-
return this.ext;
|
28
|
-
}
|
29
|
-
|
30
|
-
getId() {
|
31
|
-
return this.inatPhotoId;
|
32
|
-
}
|
33
|
-
|
34
|
-
getUrl() {
|
35
|
-
return `https://inaturalist-open-data.s3.amazonaws.com/photos/${this.inatPhotoId}/medium.${this.ext}`;
|
36
|
-
}
|
37
|
-
|
38
|
-
getSourceUrl() {
|
39
|
-
return `https://www.inaturalist.org/photos/${this.inatPhotoId}`;
|
40
|
-
}
|
41
|
-
}
|
42
|
-
|
43
|
-
export { InatPhoto };
|