@ca-plant-list/ca-plant-list 0.4.8 → 0.4.10

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/lib/inat_photo.js CHANGED
@@ -1,10 +1,4 @@
1
- import {
2
- CC0,
3
- CC_BY,
4
- CC_BY_NC,
5
- COPYRIGHT,
6
- Photo,
7
- } from "./photo.js";
1
+ import { CC0, CC_BY, CC_BY_NC, COPYRIGHT, Photo } from "./photo.js";
8
2
 
9
3
  class InatPhoto extends Photo {
10
4
  /** @type {number} */
@@ -18,17 +12,25 @@ class InatPhoto extends Photo {
18
12
  * @param {InatLicenseCode} licenseCode
19
13
  * @param {string} attrName
20
14
  */
21
- constructor( id, ext, licenseCode, attrName ) {
15
+ constructor(id, ext, licenseCode, attrName) {
22
16
  /** @type {typeof COPYRIGHT | typeof CC_BY | typeof CC_BY_NC | typeof CC0} */
23
17
  let rights = COPYRIGHT;
24
- if ( licenseCode === "cc0" ) rights = CC0;
25
- else if ( licenseCode === "cc-by" ) rights = CC_BY;
26
- else if ( licenseCode === "cc-by-nc" ) rights = CC_BY_NC;
27
- super( null, attrName, rights );
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);
28
22
  this.inatPhotoId = id;
29
23
  this.ext = ext;
30
24
  }
31
25
 
26
+ getExt() {
27
+ return this.ext;
28
+ }
29
+
30
+ getId() {
31
+ return this.inatPhotoId;
32
+ }
33
+
32
34
  getUrl() {
33
35
  return `https://inaturalist-open-data.s3.amazonaws.com/photos/${this.inatPhotoId}/medium.${this.ext}`;
34
36
  }
@@ -38,6 +40,4 @@ class InatPhoto extends Photo {
38
40
  }
39
41
  }
40
42
 
41
- export {
42
- InatPhoto
43
- };
43
+ export { InatPhoto };
@@ -0,0 +1,29 @@
1
+ import cliProgress from "cli-progress";
2
+
3
+ export class ProgressMeter {
4
+ #meter;
5
+
6
+ /**
7
+ * @param {string} label
8
+ * @param {number} total
9
+ */
10
+ constructor(label, total) {
11
+ this.#meter = new cliProgress.SingleBar({
12
+ format: `${label} {percentage}% | {value}/{total}{custom}`,
13
+ hideCursor: true,
14
+ });
15
+ this.#meter.start(total, 0, { custom: "" });
16
+ }
17
+
18
+ stop() {
19
+ this.#meter.stop();
20
+ }
21
+
22
+ /**
23
+ * @param {number} current
24
+ * @param {{ custom: string; }} [custom={ custom: "" }]
25
+ */
26
+ update(current, custom = { custom: "" }) {
27
+ this.#meter.update(current, custom);
28
+ }
29
+ }
package/lib/taxa.js CHANGED
@@ -48,7 +48,7 @@ class Taxa {
48
48
  taxonFactory = (td, g) => new Taxon(td, g),
49
49
  extraTaxa = [],
50
50
  extraSynonyms = [],
51
- includePhotos = true
51
+ includePhotos = true,
52
52
  ) {
53
53
  this.#isSubset = inclusionList !== true;
54
54
 
@@ -68,7 +68,7 @@ class Taxa {
68
68
  extraTaxa,
69
69
  inclusionList,
70
70
  taxonFactory,
71
- showFlowerErrors
71
+ showFlowerErrors,
72
72
  );
73
73
 
74
74
  // Make sure everything in the inclusionList has been loaded.
@@ -79,12 +79,11 @@ class Taxa {
79
79
  }
80
80
 
81
81
  this.#sortedTaxa = Object.values(this.#taxa).sort((a, b) =>
82
- a.getName().localeCompare(b.getName())
82
+ a.getName().localeCompare(b.getName()),
83
83
  );
84
84
 
85
-
86
- if ( includePhotos ) {
87
- this.#loadInatPhotos( dataDir );
85
+ if (includePhotos) {
86
+ this.#loadInatPhotos(dataDir);
88
87
  }
89
88
 
90
89
  const synCSV = CSV.parseFile(dataDir, "synonyms.csv");
@@ -95,22 +94,24 @@ class Taxa {
95
94
  /**
96
95
  * @param {string} dataDir
97
96
  */
98
- #loadInatPhotos( dataDir ) {
97
+ #loadInatPhotos(dataDir) {
99
98
  const photosFileName = "inattaxonphotos.csv";
100
- if ( fs.existsSync( path.join( dataDir, photosFileName ) ) ) {
99
+ if (fs.existsSync(path.join(dataDir, photosFileName))) {
101
100
  /** @type {InatCsvPhoto[]} */
102
- const csvPhotos = CSV.parseFile( dataDir, photosFileName );
103
- for ( const csvPhoto of csvPhotos ) {
101
+ const csvPhotos = CSV.parseFile(dataDir, photosFileName);
102
+ for (const csvPhoto of csvPhotos) {
104
103
  const taxon = this.getTaxon(csvPhoto.name);
105
- if(!taxon) {
104
+ if (!taxon) {
106
105
  continue;
107
106
  }
108
- taxon.addPhoto(new InatPhoto(
109
- csvPhoto.id,
110
- csvPhoto.ext,
111
- csvPhoto.licenseCode,
112
- csvPhoto.attrName
113
- ) );
107
+ taxon.addPhoto(
108
+ new InatPhoto(
109
+ csvPhoto.id,
110
+ csvPhoto.ext,
111
+ csvPhoto.licenseCode,
112
+ csvPhoto.attrName,
113
+ ),
114
+ );
114
115
  }
115
116
  }
116
117
  }
@@ -121,7 +122,7 @@ class Taxa {
121
122
 
122
123
  getFlowerColors() {
123
124
  return Object.values(this.#flower_colors).filter(
124
- (fc) => fc.getTaxa().length > 0
125
+ (fc) => fc.getTaxa().length > 0,
125
126
  );
126
127
  }
127
128
 
@@ -209,10 +210,10 @@ class Taxa {
209
210
  const color = this.#flower_colors[colorName];
210
211
  if (!color) {
211
212
  throw new Error(
212
- "flower color \"" +
213
+ 'flower color "' +
213
214
  colorName +
214
- "\" not found for " +
215
- name
215
+ '" not found for ' +
216
+ name,
216
217
  );
217
218
  }
218
219
  color.addTaxon(taxon);
@@ -229,7 +230,7 @@ class Taxa {
229
230
  ) {
230
231
  this.#errorLog.log(
231
232
  name,
232
- "does not have all flower info"
233
+ "does not have all flower info",
233
234
  );
234
235
  }
235
236
  } else {
package/lib/util.js CHANGED
@@ -3,8 +3,9 @@
3
3
  * https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore?tab=readme-ov-file#_chunk
4
4
  * @param {any[]} input
5
5
  * @param {number} size
6
+ * @returns {any[][]}
6
7
  */
7
- export function chunk( input, size ) {
8
+ export function chunk(input, size) {
8
9
  return input.reduce((arr, item, idx) => {
9
10
  return idx % size === 0
10
11
  ? [...arr, [item]]
@@ -15,6 +16,6 @@ export function chunk( input, size ) {
15
16
  /**
16
17
  * @param {number} time
17
18
  */
18
- export async function sleep( time ) {
19
- return new Promise( resolve => setTimeout( resolve, time ) );
19
+ export async function sleep(time) {
20
+ return new Promise((resolve) => setTimeout(resolve, time));
20
21
  }
@@ -0,0 +1,90 @@
1
+ import { ProgressMeter } from "../progressmeter.js";
2
+ import { chunk, sleep } from "../util.js";
3
+
4
+ const ALLOWED_LICENSE_CODES = ["cc0", "cc-by", "cc-by-nc"];
5
+
6
+ /**
7
+ * @param {Taxon[]} taxa
8
+ * @return {Promise<InatApiTaxon[]>}
9
+ */
10
+ async function fetchInatTaxa(taxa) {
11
+ const inatTaxonIDs = taxa.map((taxon) => taxon.getINatID()).filter(Boolean);
12
+ const url = `https://api.inaturalist.org/v2/taxa/${inatTaxonIDs.join(",")}?fields=(taxon_photos:(photo:(medium_url:!t,attribution:!t,license_code:!t)))`;
13
+ const resp = await fetch(url);
14
+ if (!resp.ok) {
15
+ const error = await resp.text();
16
+ throw new Error(`Failed to fetch taxa from iNat: ${error}`);
17
+ }
18
+ const json = await resp.json();
19
+ return json.results;
20
+ }
21
+
22
+ /**
23
+ * @param {Taxon[]} taxaToUpdate
24
+ * @returns {Promise<Map<string,InatPhotoInfo[]>>}
25
+ */
26
+ export async function getTaxonPhotos(taxaToUpdate) {
27
+ /** @type {Map<string,string>} */
28
+ const idMap = new Map();
29
+
30
+ for (const taxon of taxaToUpdate) {
31
+ if (taxon.getINatID()) {
32
+ idMap.set(taxon.getINatID(), taxon.getName());
33
+ }
34
+ }
35
+
36
+ /** @type {Map<string,InatPhotoInfo[]>} */
37
+ const photos = new Map();
38
+
39
+ const meter = new ProgressMeter("retrieving taxa", taxaToUpdate.length);
40
+ let taxaRetrieved = 0;
41
+
42
+ for (const batch of chunk(taxaToUpdate, 30)) {
43
+ const inatTaxa = await fetchInatTaxa(batch);
44
+ for (const iNatTaxon of inatTaxa) {
45
+ const iNatTaxonPhotos = iNatTaxon.taxon_photos
46
+ .filter((tp) =>
47
+ ALLOWED_LICENSE_CODES.includes(tp.photo.license_code),
48
+ )
49
+ .slice(0, 5);
50
+
51
+ const taxonName = idMap.get(iNatTaxon.id.toString());
52
+ if (!taxonName) {
53
+ throw new Error(`iNat id ${iNatTaxon.id} not found`);
54
+ }
55
+ /** @type {InatPhotoInfo[]} */
56
+ const taxonPhotos = [];
57
+ for (const taxonPhoto of iNatTaxonPhotos) {
58
+ const ext = taxonPhoto.photo.medium_url.split(".").at(-1);
59
+ if (!ext) {
60
+ continue;
61
+ }
62
+ /** @type {InatPhotoInfo} */
63
+ const obj = {
64
+ id: taxonPhoto.photo.id.toString(),
65
+ ext: ext,
66
+ licenseCode: taxonPhoto.photo.license_code,
67
+ attrName:
68
+ // Photographers retain copyright for most CC licenses,
69
+ // except CC0, so attribution is a bit different
70
+ taxonPhoto.photo.attribution.match(
71
+ /\(c\) (.*?),/,
72
+ )?.[1] ||
73
+ taxonPhoto.photo.attribution.match(
74
+ /uploaded by (.*)/,
75
+ )?.[1],
76
+ };
77
+
78
+ taxonPhotos.push(obj);
79
+ }
80
+ photos.set(taxonName, taxonPhotos);
81
+ }
82
+ taxaRetrieved += batch.length;
83
+ meter.update(taxaRetrieved);
84
+ await sleep(1_100);
85
+ }
86
+
87
+ meter.stop();
88
+
89
+ return photos;
90
+ }