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

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,113 @@
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 { Program } from "../lib/program.js";
9
+ import { Taxa } from "../lib/taxa.js";
10
+ import { sleep } from "../lib/util.js";
11
+
12
+ // While I'm guessing the products of this data will be non-commercial, it's
13
+ // not clear how they'll be licensed so the ShareAlike clause is out, and
14
+ // they'll probably be derivative works so the "No Derivatives" clause should
15
+ // be respected.
16
+ const ALLOWED_LICENSE_CODES = ["cc0", "cc-by", "cc-by-nc"];
17
+
18
+ const DEFAULT_FILENAME = "inatobsphotos.csv";
19
+
20
+ /**
21
+ * @param {Taxon} taxon
22
+ * @param {InatObsPhotosCommandLineOptions} options
23
+ * @return {Promise<InatApiObservation[]>}
24
+ */
25
+ async function fetchObservationsForTaxon(taxon, options) {
26
+ const inatTaxonId = taxon.getINatID();
27
+ if (!inatTaxonId) return [];
28
+ let url =
29
+ `https://api.inaturalist.org/v2/observations/?taxon_id=${inatTaxonId}` +
30
+ "&photo_license=" +
31
+ ALLOWED_LICENSE_CODES.join(",") +
32
+ "&order=desc" +
33
+ "&order_by=votes" +
34
+ "&per_page=5" +
35
+ "&fields=(observation_photos:(photo:(url:!t,attribution:!t,license_code:!t)))";
36
+ if (typeof options.inatObsQuery === "string") {
37
+ url += `&${options.inatObsQuery}`;
38
+ }
39
+ const resp = await fetch(url);
40
+ if (!resp.ok) {
41
+ const error = await resp.text();
42
+ throw new Error(`Failed to fetch taxa from iNat: ${error}`);
43
+ }
44
+ const json = await resp.json();
45
+ return json.results;
46
+ }
47
+
48
+ /**
49
+ * @param {InatObsPhotosCommandLineOptions} options
50
+ */
51
+ async function getObsPhotos(options) {
52
+ console.log("[inatobsphotos.js] options", options);
53
+
54
+ const taxa = await Taxa.loadTaxa(options);
55
+ const targetTaxa = taxa.getTaxonList();
56
+
57
+ const filename = path.join("data", options.filename || DEFAULT_FILENAME);
58
+ const writableStream = fs.createWriteStream(filename);
59
+ const columns = ["name", "id", "ext", "licenseCode", "attrName"];
60
+ const stringifier = stringify({ header: true, columns: columns });
61
+ stringifier.pipe(writableStream);
62
+ const prog = new cliProgress.SingleBar({
63
+ format: "Downloading [{bar}] {percentage}% | ETA: {eta}s | {value}/{total}",
64
+ etaBuffer: targetTaxa.length,
65
+ });
66
+ prog.setMaxListeners(100);
67
+ prog.start(targetTaxa.length, 0);
68
+
69
+ for (const taxon of targetTaxa) {
70
+ prog.increment();
71
+ const observations = await fetchObservationsForTaxon(taxon, options);
72
+ // Just get the CC-licensed ones, 5 per taxon should be fine (max is 20 on iNat). Whether or not
73
+ const photos = observations
74
+ .map((obs) => obs.observation_photos.map((op) => op.photo))
75
+ .flat()
76
+ .filter((photo) =>
77
+ ALLOWED_LICENSE_CODES.includes(photo.license_code),
78
+ )
79
+ .slice(0, 5);
80
+ for (const photo of photos) {
81
+ const row = [
82
+ taxon.getName(),
83
+ photo.id,
84
+ String(photo.url).split(".").at(-1),
85
+ // Need the license code to do attribution properly
86
+ photo.license_code,
87
+ // Photographers retain copyright for most CC licenses,
88
+ // except CC0, so attribution is a bit different
89
+ photo.attribution.match(/\(c\) (.*?),/)?.[1] ||
90
+ photo.attribution.match(/uploaded by (.*)/)?.[1],
91
+ ];
92
+ stringifier.write(row);
93
+ }
94
+ await sleep(1_100);
95
+ }
96
+ prog.stop();
97
+ }
98
+
99
+ const program = Program.getProgram();
100
+ program
101
+ .action(getObsPhotos)
102
+ .description("Write a CSV to datadir with iNaturalist observation photos")
103
+ .option(
104
+ "-q, --inat-obs-query <query>",
105
+ "Additional iNat observations API query terms to add, e.g. place_id=1234&d1=2020-01-01",
106
+ )
107
+ .option(
108
+ "-fn, --filename <filename>",
109
+ "Name of file to write to the data dir",
110
+ DEFAULT_FILENAME,
111
+ );
112
+
113
+ await program.parseAsync();
@@ -18,6 +18,8 @@ const ALLOWED_LICENSE_CODES = [
18
18
  "cc0", "cc-by", "cc-by-nc"
19
19
  ];
20
20
 
21
+ const DEFAULT_FILENAME = "inattaxonphotos.csv";
22
+
21
23
  /**
22
24
  * @param {Taxon[]} taxa
23
25
  * @return {Promise<InatApiTaxon[]>}
@@ -35,7 +37,7 @@ async function fetchInatTaxa( taxa ) {
35
37
  }
36
38
 
37
39
  /**
38
- * @param {CommandLineOptions} options
40
+ * @param {InatTaxonPhotosCommandLineOptions} options
39
41
  */
40
42
  async function getTaxonPhotos( options ) {
41
43
  const errorLog = new ErrorLog(options.outputdir + "/errors.tsv");
@@ -46,7 +48,7 @@ async function getTaxonPhotos( options ) {
46
48
  );
47
49
  const targetTaxa = taxa.getTaxonList( );
48
50
 
49
- const filename = path.join( "data", "inattaxonphotos.csv" );
51
+ const filename = path.join("data", options.filename || DEFAULT_FILENAME);
50
52
  const writableStream = fs.createWriteStream( filename );
51
53
  const columns = [
52
54
  "name",
@@ -80,7 +82,7 @@ async function getTaxonPhotos( options ) {
80
82
  const row = [
81
83
  taxon.getName(),
82
84
  taxonPhoto.photo.id,
83
- taxonPhoto.photo.medium_url.split( "." ).at( -1 ),
85
+ String( taxonPhoto.photo.medium_url ).split( "." ).at( -1 ),
84
86
  // Need the license code to do attribution properly
85
87
  taxonPhoto.photo.license_code,
86
88
  // Photographers retain copyright for most CC licenses,
@@ -101,6 +103,10 @@ async function getTaxonPhotos( options ) {
101
103
  }
102
104
 
103
105
  const program = Program.getProgram();
104
- program.action(getTaxonPhotos).description( "Write a CSV to datadir with iNaturalist taxon photos" );
106
+ program.action(getTaxonPhotos).description( "Write a CSV to datadir with iNaturalist taxon photos" )
107
+ .option(
108
+ "-fn, --filename <filename>",
109
+ "Name of file to write to the data dir"
110
+ );
105
111
 
106
112
  await program.parseAsync();
@@ -16,7 +16,7 @@ declare class Config {
16
16
  }
17
17
 
18
18
  declare class ErrorLog {
19
- log(...args: string[]): void;
19
+ log(...args: any[]): void;
20
20
  write(): void;
21
21
  }
22
22
 
@@ -83,6 +83,7 @@ declare class Taxa {
83
83
  getFlowerColors(): FlowerColor[];
84
84
  getTaxon(name: string): Taxon;
85
85
  getTaxonList(): Taxon[];
86
+ hasSynonym(name: string): boolean;
86
87
  isSubset(): boolean;
87
88
  }
88
89
 
@@ -92,6 +93,7 @@ declare class TaxaCol {
92
93
  title: string;
93
94
  }
94
95
 
96
+ type StatusCode = "N" | "NC" | "U" | "X";
95
97
  declare class Taxon {
96
98
  constructor(data: TaxonData, genera: Genera, meta: any);
97
99
  getBaseFileName(): string;
@@ -103,6 +105,7 @@ declare class Taxon {
103
105
  getCalscapeCommonName(): string | undefined;
104
106
  getCalscapeName(): string;
105
107
  getCESA(): string | undefined;
108
+ getCNDDBRank(): string | undefined;
106
109
  getCommonNames(): string[];
107
110
  getFamily(): Family;
108
111
  getFESA(): string | undefined;
@@ -110,21 +113,25 @@ declare class Taxon {
110
113
  getFlowerColors(): string[] | undefined;
111
114
  getGenus(): Genus;
112
115
  getGenusName(): string;
116
+ getGlobalRank(): string | undefined;
113
117
  getHTMLLink(
114
118
  href: boolean | string | undefined,
115
119
  includeRPI?: boolean,
116
120
  ): string;
117
121
  getINatID(): string;
118
122
  getINatName(): string;
123
+ getINatSyn(): string | undefined;
119
124
  getINatTaxonLink(): string;
120
125
  getJepsonID(): string;
121
126
  getLifeCycle(): string;
122
127
  getName(): string;
123
128
  getPhotos(): Photo[];
129
+ getRPIID(): string | undefined;
124
130
  getRPIRank(): string;
125
131
  getRPIRankAndThreat(): string;
126
132
  getRPIRankAndThreatTooltip(): string;
127
133
  getRPITaxonLink(): string;
134
+ getStatus(): StatusCode;
128
135
  getStatusDescription(config: Config): string;
129
136
  getSynonyms(): string[];
130
137
  isCANative(): boolean;
@@ -147,7 +154,7 @@ declare class TaxonData {
147
154
  life_cycle: string;
148
155
  "RPI ID": string;
149
156
  SRank: string;
150
- status: string;
157
+ status: StatusCode;
151
158
  taxon_name: string;
152
159
  }
153
160
 
@@ -194,14 +201,32 @@ declare class InatCsvPhoto {
194
201
  attrName: string;
195
202
  }
196
203
 
204
+ declare class InatApiPhoto {
205
+ id: number;
206
+ attribution: string;
207
+ license_code: InatLicenseCode;
208
+ medium_url?: string;
209
+ url?: string;
210
+ }
211
+
197
212
  declare class InatApiTaxon {
198
213
  id: number;
199
214
  taxon_photos: {
200
- photo: {
201
- id: number;
202
- attribution: string;
203
- license_code: InatLicenseCode;
204
- medium_url: string;
205
- };
215
+ photo: InatApiPhoto;
206
216
  }[];
207
217
  }
218
+
219
+ declare class InatApiObservation {
220
+ observation_photos: {
221
+ photo: InatApiPhoto;
222
+ }[]
223
+ }
224
+
225
+ declare class InatObsPhotosCommandLineOptions extends CommandLineOptions {
226
+ filename?: string;
227
+ inatObsQuery?: string;
228
+ }
229
+
230
+ declare class InatTaxonPhotosCommandLineOptions extends CommandLineOptions {
231
+ filename?: string;
232
+ }
@@ -1,20 +0,0 @@
1
- on:
2
- workflow_dispatch:
3
- pull_request:
4
- push:
5
-
6
- permissions:
7
- contents: read
8
-
9
- jobs:
10
- check:
11
- runs-on: ubuntu-latest
12
- steps:
13
- - uses: actions/checkout@v4
14
- - uses: actions/setup-node@v4
15
- with:
16
- node-version: latest
17
- cache: "npm"
18
- - run: npm update
19
- - run: npx eslint
20
- - run: npx tsc
package/.prettierrc DELETED
@@ -1,3 +0,0 @@
1
- {
2
- "tabWidth": 4
3
- }
@@ -1,9 +0,0 @@
1
- {
2
- "eslint.validate": ["javascript", "typescript"],
3
- "json.schemas": [
4
- {
5
- "fileMatch": ["exceptions.json"],
6
- "url": "./schemas/exceptions.schema.json"
7
- }
8
- ]
9
- }
package/eslint.config.mjs DELETED
@@ -1,13 +0,0 @@
1
- import globals from "globals";
2
- import pluginJs from "@eslint/js";
3
-
4
- /** @type {import('eslint').Linter.Config[]} */
5
- export default [
6
- { files: ["**/*.{js,mjs,cjs}"], ignores: ["output/**"] },
7
- {
8
- languageOptions: {
9
- globals: { ...globals.browser, ...globals.node, bootstrap: false },
10
- },
11
- },
12
- pluginJs.configs.recommended,
13
- ];
@@ -1,62 +0,0 @@
1
- {
2
- "type": "object",
3
- "additionalProperties": {
4
- "type": "object",
5
- "properties": {
6
- "calflora": {
7
- "type": "object",
8
- "properties": {
9
- "badjepsonid": {
10
- "const": true
11
- },
12
- "native": { "type": "boolean" },
13
- "notintaxondata": {
14
- "const": true
15
- }
16
- },
17
- "additionalProperties": false
18
- },
19
- "calscape": {
20
- "type": "object",
21
- "properties": { "notnative": { "const": true } }
22
- },
23
- "comment": {
24
- "type": "string"
25
- },
26
- "inat": {
27
- "type": "object",
28
- "properties": {
29
- "notintaxondata": {
30
- "const": true
31
- }
32
- },
33
- "additionalProperties": false
34
- },
35
- "jepson": {
36
- "type": "object",
37
- "properties": {
38
- "allowsynonym": {
39
- "const": true
40
- },
41
- "notineflora": {
42
- "const": true
43
- }
44
- },
45
- "additionalProperties": false
46
- },
47
- "rpi": {
48
- "type": "object",
49
- "properties": {
50
- "translation": {
51
- "type": "string"
52
- },
53
- "translation-to-rpi": {
54
- "type": "string"
55
- }
56
- },
57
- "additionalProperties": false
58
- }
59
- },
60
- "additionalProperties": false
61
- }
62
- }
package/tmp/config.json DELETED
@@ -1,21 +0,0 @@
1
- {
2
- "calflora": {
3
- "counties": [
4
- "ALA",
5
- "CCA"
6
- ]
7
- },
8
- "inat": {
9
- "project": "ebcnps"
10
- },
11
- "labels": {
12
- "introduced": "Introduced to the East Bay",
13
- "native": "Native to the East Bay",
14
- "status-NC": "California native introduced to the East Bay"
15
- },
16
- "ebook": {
17
- "filename": "ebplants",
18
- "pub_id": "ebplants",
19
- "title": "East Bay Plants"
20
- }
21
- }
@@ -1,93 +0,0 @@
1
- {
2
- "Campanula exigua": {
3
- "rpi": {
4
- "translation-to-rpi": "Ravenella exigua"
5
- }
6
- },
7
- "Campanula sharsmithiae": {
8
- "rpi": {
9
- "translation-to-rpi": "Ravenella sharsmithiae"
10
- }
11
- },
12
- "Castilleja ambigua subsp. ambigua": {
13
- "rpi": {
14
- "translation-to-rpi": "Castilleja ambigua var. ambigua"
15
- }
16
- },
17
- "Castilleja ambigua var. ambigua": {
18
- "rpi": {
19
- "translation": "Castilleja ambigua subsp. ambigua"
20
- }
21
- },
22
- "Downingia ornatissima var. mirabilis": {
23
- "inat": {
24
- "notintaxondata": true
25
- }
26
- },
27
- "Erysimum capitatum var. angustatum": {
28
- "calflora": {
29
- "badjepsonid": true
30
- },
31
- "jepson": {
32
- "allowsynonym": true
33
- }
34
- },
35
- "Heterotheca oregona var. rudis": {
36
- "inat": {
37
- "notintaxondata": true
38
- }
39
- },
40
- "Horkelia californica var. frondosa": {
41
- "inat": {
42
- "notintaxondata": true
43
- }
44
- },
45
- "Lupinus littoralis var. variicolor": {
46
- "inat": {
47
- "notintaxondata": true
48
- }
49
- },
50
- "Malacothamnus hallii": {
51
- "comment": "in CNPS Rare Plant Inventory",
52
- "calflora": {
53
- "notintaxondata": true
54
- },
55
- "jepson": {
56
- "allowsynonym": true
57
- },
58
- "rpi": {
59
- "translation": "Malacothamnus arcuatus var. elmeri"
60
- }
61
- },
62
- "Malacothamnus arcuatus var. elmeri": {
63
- "comment": "in CNPS Rare Plant Inventory as Malacothamnus hallii",
64
- "rpi": {
65
- "translation-to-rpi": "Malacothamnus hallii"
66
- }
67
- },
68
- "Myosurus minimus subsp. apus": {
69
- "comment": "in CNPS Rare Plant Inventory",
70
- "jepson": {
71
- "notineflora": true
72
- }
73
- },
74
- "Ravenella exigua": {
75
- "comment": "in CNPS Rare Plant Inventory as Ravenella",
76
- "rpi": {
77
- "translation": "Campanula exigua"
78
- }
79
- },
80
- "Ravenella sharsmithiae": {
81
- "rpi": {
82
- "translation": "Campanula sharsmithiae"
83
- }
84
- },
85
- "Streptanthus albidus subsp. peramoenus": {
86
- "calflora": {
87
- "notintaxondata": true
88
- },
89
- "jepson": {
90
- "allowsynonym": true
91
- }
92
- }
93
- }