@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.
Files changed (51) hide show
  1. package/data/genera.json +36 -32
  2. package/data/inattaxonphotos.csv +4 -0
  3. package/data/synonyms.csv +2 -0
  4. package/data/taxa.csv +6 -5
  5. package/data/text/Polypodium-calirhiza.md +1 -0
  6. package/data/text/Polypodium-scouleri.md +1 -0
  7. package/data/text/Rumex-conglomeratus.md +1 -0
  8. package/data/text/Rumex-obtusifolius.md +1 -0
  9. package/data/text/Rumex-pulcher.md +1 -0
  10. package/data/text/Rumex-salicifolius.md +1 -0
  11. package/lib/basepagerenderer.js +3 -3
  12. package/lib/ebook/glossarypages.js +3 -3
  13. package/lib/ebook/images.js +7 -7
  14. package/lib/ebook/pages/page_list_families.js +1 -1
  15. package/lib/ebook/pages/page_list_flower_color.js +2 -2
  16. package/lib/ebook/pages/page_list_flowers.js +8 -8
  17. package/lib/ebook/pages/page_list_species.js +1 -1
  18. package/lib/ebook/pages/taxonpage.js +2 -2
  19. package/lib/ebook/pages/tocpage.js +2 -2
  20. package/lib/ebook/plantbook.js +3 -3
  21. package/lib/externalsites.js +20 -15
  22. package/lib/families.js +14 -14
  23. package/lib/flowercolor.js +2 -2
  24. package/lib/genera.js +3 -3
  25. package/lib/htmltaxon.js +16 -7
  26. package/lib/index.d.ts +46 -1
  27. package/lib/pagerenderer.js +223 -220
  28. package/lib/photo.js +52 -31
  29. package/lib/plants/glossary.js +2 -4
  30. package/lib/program.js +10 -2
  31. package/lib/sitegenerator.js +13 -3
  32. package/lib/taxa.js +16 -12
  33. package/lib/taxon.js +12 -6
  34. package/lib/tools/calflora.js +41 -8
  35. package/lib/tools/calscape.js +4 -4
  36. package/lib/tools/inat.js +7 -7
  37. package/lib/tools/jepsoneflora.js +28 -4
  38. package/lib/tools/jepsonfamilies.js +102 -0
  39. package/lib/tools/rpi.js +8 -9
  40. package/lib/tools/supplementaltext.js +43 -0
  41. package/lib/tools/taxacsv.js +2 -2
  42. package/lib/utils/inat-tools.js +39 -2
  43. package/lib/web/glossarypages.js +6 -6
  44. package/lib/web/pagetaxon.js +4 -6
  45. package/package.json +10 -8
  46. package/scripts/cpl-photos.js +2 -2
  47. package/scripts/cpl-tools.js +11 -3
  48. package/scripts/inatobsphotos.js +10 -1
  49. package/scripts/inattaxonphotos.js +45 -43
  50. package/lib/inat_photo.js +0 -43
  51. package/types/classes.d.ts +0 -232
package/lib/photo.js CHANGED
@@ -1,44 +1,65 @@
1
- const CC0 = "CC0";
2
- const CC_BY = "CC BY";
3
- const CC_BY_NC = "CC BY-NC";
4
- const COPYRIGHT = "C";
1
+ /**
2
+ * @typedef {"CC0" | "CC BY" | "CC BY-NC" | "C" | null} PhotoRights
3
+ */
5
4
 
6
- class Photo {
7
- /** @type {string?} */
8
- #url;
9
- /** @type {string?} */
10
- rightsHolder;
11
- /** @type {null | typeof COPYRIGHT | typeof CC_BY | typeof CC_BY_NC | typeof CC0} */
12
- rights;
5
+ export class Photo {
6
+ #id;
7
+ #ext;
8
+ #rightsHolder;
9
+ /** @type {PhotoRights} */
10
+ #rights;
13
11
 
14
12
  /**
15
- * @param {string?} url
16
- * @param {string?} rightsHolder
17
- * @param {null | typeof COPYRIGHT | typeof CC_BY | typeof CC_BY_NC | typeof CC0} rights
13
+ * @param {number} id
14
+ * @param {string} ext
15
+ * @param {import("./utils/inat-tools.js").InatLicenseCode} licenseCode
16
+ * @param {string} rightsHolder
18
17
  */
19
- constructor( url, rightsHolder, rights ) {
20
- this.#url = url;
21
- this.rightsHolder = rightsHolder;
22
- this.rights = rights;
18
+ constructor(id, ext, licenseCode, rightsHolder) {
19
+ this.#id = id;
20
+ this.#ext = ext;
21
+ this.#rightsHolder = rightsHolder;
22
+ if (licenseCode === "cc0") this.#rights = "CC0";
23
+ else if (licenseCode === "cc-by") this.#rights = "CC BY";
24
+ else if (licenseCode === "cc-by-nc") this.#rights = "CC BY-NC";
25
+ else this.#rights = "C";
23
26
  }
24
27
 
25
- getUrl() {
26
- return this.#url;
28
+ /**
29
+ * @returns {string}
30
+ */
31
+ getAttribution() {
32
+ if (this.#rights === "CC0") {
33
+ if (this.#rightsHolder) {
34
+ return `By ${this.#rightsHolder} (${this.#rights})`;
35
+ }
36
+ return this.#rights;
37
+ }
38
+ if (this.#rightsHolder) {
39
+ return `(c) ${this.#rightsHolder} (${this.#rights})`;
40
+ }
41
+ return `(c) (${this.#rights})`;
42
+ }
43
+
44
+ getExt() {
45
+ return this.#ext;
46
+ }
47
+
48
+ getId() {
49
+ return this.#id;
27
50
  }
28
51
 
29
52
  /**
30
- * Return URL of page from whence this photo came
31
- * @return {string?}
53
+ * @returns {string} The URL of the iNaturalist page with details about the image.
32
54
  */
33
55
  getSourceUrl() {
34
- return null;
56
+ return `https://www.inaturalist.org/photos/${this.#id}`;
35
57
  }
36
- }
37
58
 
38
- export {
39
- CC0,
40
- CC_BY,
41
- CC_BY_NC,
42
- COPYRIGHT,
43
- Photo
44
- };
59
+ /**
60
+ * @returns {string} The URL to retrieve the image file.
61
+ */
62
+ getUrl() {
63
+ return `https://inaturalist-open-data.s3.amazonaws.com/photos/${this.#id}/medium.${this.#ext}`;
64
+ }
65
+ }
@@ -1,7 +1,7 @@
1
1
  import { Config } from "../config.js";
2
2
  import { Files } from "../files.js";
3
3
 
4
- class Glossary {
4
+ export class Glossary {
5
5
  #srcPath;
6
6
  /** @type {GlossaryEntry[]} */
7
7
  #srcEntries = [];
@@ -21,7 +21,7 @@ class Glossary {
21
21
  }
22
22
  }
23
23
 
24
- class GlossaryEntry {
24
+ export class GlossaryEntry {
25
25
  #srcPath;
26
26
  #fileName;
27
27
  #term;
@@ -48,5 +48,3 @@ class GlossaryEntry {
48
48
  return this.#term;
49
49
  }
50
50
  }
51
-
52
- export { Glossary };
package/lib/program.js CHANGED
@@ -3,6 +3,14 @@ import { Files } from "./files.js";
3
3
  import { CSV } from "./csv.js";
4
4
  import path from "node:path";
5
5
 
6
+ /**
7
+ * @typedef {{
8
+ datadir: string;
9
+ outputdir: string;
10
+ "show-flower-errors": boolean;
11
+ }} CommandLineOptions
12
+ */
13
+
6
14
  class Program {
7
15
  /**
8
16
  * @param {string} dataDir
@@ -16,10 +24,10 @@ class Program {
16
24
  return true;
17
25
  }
18
26
 
19
- /** @type {TaxonData[]} */
27
+ /** @type {import("./index.js").TaxonData[]} */
20
28
  // @ts-ignore
21
29
  const includeCSV = CSV.readFile(path.join(dataDir, includeFileName));
22
- /** @type {Object<string,TaxonData>} */
30
+ /** @type {Object<string,import("./index.js").TaxonData>} */
23
31
  const include = {};
24
32
  for (const row of includeCSV) {
25
33
  include[row["taxon_name"]] = row;
@@ -14,12 +14,12 @@ class SiteGenerator {
14
14
  }
15
15
 
16
16
  /**
17
- * @param {FlowerColor[]} flowerColors
17
+ * @param {import("./flowercolor.js").FlowerColor[]} flowerColors
18
18
  */
19
19
  copyIllustrations(flowerColors) {
20
20
  /**
21
21
  * @param {string} outputDir
22
- * @param {FlowerColor[]} flowerColors
22
+ * @param {import("./flowercolor.js").FlowerColor[]} flowerColors
23
23
  */
24
24
  function createFlowerColorIcons(outputDir, flowerColors) {
25
25
  // Read generic input.
@@ -28,7 +28,7 @@ class SiteGenerator {
28
28
  for (const color of flowerColors) {
29
29
  Files.write(
30
30
  Files.join(outputDir, "f-" + color.getColorName() + ".svg"),
31
- srcSVG.replace("#ff0", color.getColorCode())
31
+ srcSVG.replace("#ff0", color.getColorCode()),
32
32
  );
33
33
  }
34
34
  // Delete input file.
@@ -69,6 +69,16 @@ class SiteGenerator {
69
69
  mkdir(path) {
70
70
  Files.mkdir(Files.join(this.#baseDir, path));
71
71
  }
72
+
73
+ /**
74
+ * @param {string} content
75
+ * @param {{title:string}} attributes
76
+ * @param {string} filename
77
+ */
78
+ // eslint-disable-next-line no-unused-vars
79
+ writeTemplate(content, attributes, filename) {
80
+ throw new Error("must be implemented by subclass");
81
+ }
72
82
  }
73
83
 
74
84
  export { SiteGenerator };
package/lib/taxa.js CHANGED
@@ -7,10 +7,14 @@ import { Genera } from "./genera.js";
7
7
  import { Taxon } from "./taxon.js";
8
8
  import { Families } from "./families.js";
9
9
  import { FlowerColor } from "./flowercolor.js";
10
- import { InatPhoto } from "./inat_photo.js";
11
10
  import { TaxaCSV } from "./tools/taxacsv.js";
12
11
  import { ErrorLog } from "./errorlog.js";
13
12
  import { Program } from "./program.js";
13
+ import { Photo } from "./photo.js";
14
+
15
+ /**
16
+ * @typedef {{Current: string;Former: string;Type: string;}} SynonymData
17
+ */
14
18
 
15
19
  const FLOWER_COLORS = [
16
20
  { name: "white", color: "white" },
@@ -36,11 +40,11 @@ class Taxa {
36
40
  #isSubset;
37
41
 
38
42
  /**
39
- * @param {Object<string,TaxonData>|true} inclusionList
43
+ * @param {Object<string,import("./index.js").TaxonData>|true} inclusionList
40
44
  * @param {ErrorLog} errorLog
41
45
  * @param {boolean} showFlowerErrors
42
- * @param {function(TaxonData,Genera):Taxon} taxonFactory
43
- * @param {TaxonData[]} [extraTaxa=[]]
46
+ * @param {function(import("./index.js").TaxonData,Genera):Taxon} taxonFactory
47
+ * @param {import("./index.js").TaxonData[]} [extraTaxa=[]]
44
48
  * @param {SynonymData[]} [extraSynonyms=[]]
45
49
  * @param {boolean} includePhotos
46
50
  */
@@ -110,9 +114,9 @@ class Taxa {
110
114
  */
111
115
  #loadPhotosFromFile(dataDir, filename) {
112
116
  if (!fs.existsSync(path.join(dataDir, filename))) return;
113
- /** @type {InatCsvPhoto[]} */
117
+ /** @type {import("./utils/inat-tools.js").InatCsvPhoto[]} */
114
118
  const csvPhotos = CSV.parseFile(dataDir, filename).map((row) => {
115
- /** @type {InatLicenseCode} */
119
+ /** @type {import("./utils/inat-tools.js").InatLicenseCode} */
116
120
  let licenseCode = "cc-by";
117
121
  if (row.licenseCode === "cc-by-nc-sa") licenseCode = "cc-by-nc-sa";
118
122
  else if (row.licenseCode === "cc-by-nc") licenseCode = "cc-by-nc";
@@ -138,7 +142,7 @@ class Taxa {
138
142
  continue;
139
143
  }
140
144
  taxon.addPhoto(
141
- new InatPhoto(
145
+ new Photo(
142
146
  csvPhoto.id,
143
147
  csvPhoto.ext,
144
148
  csvPhoto.licenseCode,
@@ -220,7 +224,7 @@ class Taxa {
220
224
 
221
225
  /**
222
226
  * @param {SynonymData[]} synCSV
223
- * @param {Object<string,TaxonData>|boolean} inclusionList
227
+ * @param {Object<string,import("./index.js").TaxonData>|boolean} inclusionList
224
228
  */
225
229
  #loadSyns(synCSV, inclusionList) {
226
230
  for (const syn of synCSV) {
@@ -241,9 +245,9 @@ class Taxa {
241
245
  }
242
246
 
243
247
  /**
244
- * @param {TaxonData[]} taxaCSV
245
- * @param {Object<string,TaxonData>|true} inclusionList
246
- * @param {function(TaxonData,Genera):Taxon} taxonFactory
248
+ * @param {import("./index.js").TaxonData[]} taxaCSV
249
+ * @param {Object<string,import("./index.js").TaxonData>|true} inclusionList
250
+ * @param {function(import("./index.js").TaxonData,Genera):Taxon} taxonFactory
247
251
  * @param {Genera} genera
248
252
  * @param {boolean} showFlowerErrors
249
253
  */
@@ -251,7 +255,7 @@ class Taxa {
251
255
  for (const row of taxaCSV) {
252
256
  const name = row["taxon_name"];
253
257
 
254
- /** @type {TaxonData|{status?:StatusCode}} */
258
+ /** @type {import("./index.js").TaxonData|{status?:import("./taxon.js").StatusCode}} */
255
259
  let taxon_overrides = {};
256
260
  if (inclusionList !== true) {
257
261
  taxon_overrides = inclusionList[name];
package/lib/taxon.js CHANGED
@@ -1,6 +1,10 @@
1
1
  import { HTML } from "./html.js";
2
2
  import { RarePlants } from "./rareplants.js";
3
3
 
4
+ /**
5
+ * @typedef {"N" | "NC" | "U" | "X"} StatusCode
6
+ */
7
+
4
8
  const TAXA_COLNAMES = {
5
9
  BLOOM_START: "bloom_start",
6
10
  BLOOM_END: "bloom_end",
@@ -9,7 +13,6 @@ const TAXA_COLNAMES = {
9
13
  };
10
14
 
11
15
  class Taxon {
12
- /** @type {Genera} */
13
16
  #genera;
14
17
  #name;
15
18
  #genus;
@@ -35,12 +38,12 @@ class Taxon {
35
38
  #rankGlobal;
36
39
  /** @type {string[]} */
37
40
  #synonyms = [];
38
- /** @type {Photo[]} */
41
+ /** @type {import("./photo.js").Photo[]}*/
39
42
  #photos = [];
40
43
 
41
44
  /**
42
- * @param {TaxonData} data
43
- * @param {Genera} genera
45
+ * @param {import("./index.js").TaxonData} data
46
+ * @param {import("./genera.js").Genera} genera
44
47
  */
45
48
  constructor(data, genera) {
46
49
  this.#genera = genera;
@@ -98,12 +101,15 @@ class Taxon {
98
101
  }
99
102
 
100
103
  /**
101
- * @param {InatPhoto} photo
104
+ * @param {import("./photo.js").Photo} photo
102
105
  */
103
106
  addPhoto(photo) {
104
107
  this.#photos = this.#photos.concat([photo]);
105
108
  }
106
109
 
110
+ /**
111
+ * @returns {import("./photo.js").Photo[]}
112
+ */
107
113
  getPhotos() {
108
114
  return this.#photos;
109
115
  }
@@ -325,7 +331,7 @@ class Taxon {
325
331
  }
326
332
 
327
333
  /**
328
- * @param {Config} config
334
+ * @param {import("./config.js").Config} config
329
335
  * @returns {string}
330
336
  */
331
337
  getStatusDescription(config) {
@@ -1,6 +1,7 @@
1
1
  import * as path from "path";
2
2
  import { CSV } from "../csv.js";
3
3
  import { Files } from "../files.js";
4
+ import { TaxaCSV } from "./taxacsv.js";
4
5
 
5
6
  const CALFLORA_URL_ALL =
6
7
  "https://www.calflora.org/app/downtext?xun=117493&table=species&format=Tab&cols=0,1,4,5,8,38,41,43&psp=lifeform::grass,Tree,Herb,Fern,Shrub,Vine!!&par=f&active=";
@@ -17,18 +18,27 @@ const CALFLORA_URL_COUNTY =
17
18
  * }} CalfloraData
18
19
  */
19
20
 
20
- class Calflora {
21
+ export class Calflora {
21
22
  /** @type {Object<string,CalfloraData>} */
22
23
  static #taxa = {};
23
24
 
24
25
  /**
25
26
  *
26
27
  * @param {string} toolsDataDir
27
- * @param {Taxa} taxa
28
+ * @param {string} dataDir
29
+ * @param {import("../taxa.js").Taxa} taxa
28
30
  * @param {import("../exceptions.js").Exceptions} exceptions
29
- * @param {ErrorLog} errorLog
31
+ * @param {import("../errorlog.js").ErrorLog} errorLog
32
+ * @param {boolean} update
30
33
  */
31
- static async analyze(toolsDataDir, taxa, exceptions, errorLog) {
34
+ static async analyze(
35
+ toolsDataDir,
36
+ dataDir,
37
+ taxa,
38
+ exceptions,
39
+ errorLog,
40
+ update,
41
+ ) {
32
42
  /**
33
43
  * @param {string} url
34
44
  * @param {string} targetFile
@@ -78,6 +88,8 @@ class Calflora {
78
88
  this.#taxa[row["Taxon"]] = row;
79
89
  }
80
90
 
91
+ const idsToUpdate = new Map();
92
+
81
93
  for (const taxon of taxa.getTaxonList()) {
82
94
  const name = taxon.getName();
83
95
  if (name.includes(" unknown")) {
@@ -152,16 +164,21 @@ class Calflora {
152
164
  cfID,
153
165
  taxon.getCalfloraID(),
154
166
  );
167
+ idsToUpdate.set(name, cfID);
155
168
  }
156
169
  }
157
170
 
158
171
  this.#checkExceptions(taxa, exceptions, errorLog);
172
+
173
+ if (update) {
174
+ this.#updateIds(dataDir, idsToUpdate);
175
+ }
159
176
  }
160
177
 
161
178
  /**
162
- * @param {Taxa} taxa
179
+ * @param {import("../taxa.js").Taxa} taxa
163
180
  * @param {import("../exceptions.js").Exceptions} exceptions
164
- * @param {ErrorLog} errorLog
181
+ * @param {import("../errorlog.js").ErrorLog} errorLog
165
182
  */
166
183
  static #checkExceptions(taxa, exceptions, errorLog) {
167
184
  // Check the Calflora exceptions and make sure they still apply.
@@ -220,6 +237,22 @@ class Calflora {
220
237
  }
221
238
  }
222
239
  }
223
- }
224
240
 
225
- export { Calflora };
241
+ /**
242
+ * @param {string} dataDir
243
+ * @param {Map<string,string>} idsToUpdate
244
+ */
245
+ static #updateIds(dataDir, idsToUpdate) {
246
+ const taxa = new TaxaCSV(dataDir);
247
+
248
+ for (const taxonData of taxa.getTaxa()) {
249
+ const id = idsToUpdate.get(taxonData.taxon_name);
250
+ if (!id) {
251
+ continue;
252
+ }
253
+ taxonData["calrecnum"] = id;
254
+ }
255
+
256
+ taxa.write();
257
+ }
258
+ }
@@ -8,9 +8,9 @@ export class Calscape {
8
8
  /**
9
9
  * @param {string} toolsDataDir
10
10
  * @param {string} dataDir
11
- * @param {Taxa} taxa
11
+ * @param {import("../taxa.js").Taxa} taxa
12
12
  * @param {import("../exceptions.js").Exceptions} exceptions
13
- * @param {ErrorLog} errorLog
13
+ * @param {import("../errorlog.js").ErrorLog} errorLog
14
14
  * @param {boolean} update
15
15
  */
16
16
  static async analyze(
@@ -58,9 +58,9 @@ export class Calscape {
58
58
  }
59
59
 
60
60
  /**
61
- * @param {Taxa} taxa
61
+ * @param {import("../taxa.js").Taxa} taxa
62
62
  * @param {import("../exceptions.js").Exceptions} exceptions
63
- * @param {ErrorLog} errorLog
63
+ * @param {import("../errorlog.js").ErrorLog} errorLog
64
64
  */
65
65
  function checkExceptions(taxa, exceptions, errorLog) {
66
66
  // Check the Calscape exceptions and make sure they still apply.
package/lib/tools/inat.js CHANGED
@@ -21,9 +21,9 @@ export class INat {
21
21
  /**
22
22
  * @param {string} toolsDataDir
23
23
  * @param {string} dataDir
24
- * @param {Taxa} taxa
24
+ * @param {import("../taxa.js").Taxa} taxa
25
25
  * @param {import("../exceptions.js").Exceptions} exceptions
26
- * @param {ErrorLog} errorLog
26
+ * @param {import("../errorlog.js").ErrorLog} errorLog
27
27
  * @param {string} csvFileName
28
28
  * @param {boolean} update
29
29
  */
@@ -115,9 +115,9 @@ export class INat {
115
115
 
116
116
  /**
117
117
  *
118
- * @param {Taxa} taxa
118
+ * @param {import("../taxa.js").Taxa} taxa
119
119
  * @param {import("../exceptions.js").Exceptions} exceptions
120
- * @param {ErrorLog} errorLog
120
+ * @param {import("../errorlog.js").ErrorLog} errorLog
121
121
  */
122
122
  static #checkExceptions(taxa, exceptions, errorLog) {
123
123
  // Check the iNat exceptions and make sure they still apply.
@@ -168,9 +168,9 @@ export class INat {
168
168
 
169
169
  /**
170
170
  *
171
- * @param {Taxa} taxa
171
+ * @param {import("../taxa.js").Taxa} taxa
172
172
  * @param {import("../exceptions.js").Exceptions} exceptions
173
- * @param {ErrorLog} errorLog
173
+ * @param {import("../errorlog.js").ErrorLog} errorLog
174
174
  * @param {string} name
175
175
  * @param {string} iNatName
176
176
  */
@@ -255,7 +255,7 @@ export class INat {
255
255
 
256
256
  /**
257
257
  * @param {{name:string,rank:string}} iNatResult
258
- * @param {ErrorLog} errorLog
258
+ * @param {import("../errorlog.js").ErrorLog} errorLog
259
259
  */
260
260
  static makeSynonymName(iNatResult, errorLog) {
261
261
  const synParts = iNatResult.name.split(" ");
@@ -1,6 +1,7 @@
1
1
  import { scrape } from "@htmltools/scrape";
2
2
  import { Files } from "../files.js";
3
3
  import { SynCSV } from "./syncsv.js";
4
+ import { TaxaCSV } from "./taxacsv.js";
4
5
 
5
6
  /**
6
7
  * @typedef {{
@@ -53,8 +54,8 @@ export class JepsonEFlora {
53
54
 
54
55
  /**
55
56
  * @param {string} toolsDataDir
56
- * @param {Taxa} taxa
57
- * @param {ErrorLog} errorLog
57
+ * @param {import("../taxa.js").Taxa} taxa
58
+ * @param {import("../errorlog.js").ErrorLog} errorLog
58
59
  */
59
60
  constructor(toolsDataDir, taxa, errorLog) {
60
61
  this.#toolsDataPath = toolsDataDir + "/jepson-eflora";
@@ -63,16 +64,19 @@ export class JepsonEFlora {
63
64
  }
64
65
 
65
66
  /**
67
+ * @param {string} dataDir
66
68
  * @param {import("../exceptions.js").Exceptions} exceptions
67
69
  * @param {boolean} update
68
70
  */
69
- async analyze(exceptions, update) {
71
+ async analyze(dataDir, exceptions, update) {
70
72
  // Create data directory if it's not there.
71
73
  Files.mkdir(this.#toolsDataPath);
72
74
 
73
75
  // Retrieve all Jepson indexes.
74
76
  await this.#loadIndexPages();
75
77
 
78
+ const idsToUpdate = new Map();
79
+
76
80
  for (const taxon of this.#taxa.getTaxonList()) {
77
81
  const name = taxon.getName();
78
82
  if (name.includes(" unknown")) {
@@ -95,6 +99,7 @@ export class JepsonEFlora {
95
99
  taxon.getJepsonID(),
96
100
  jepsInfo.id,
97
101
  );
102
+ idsToUpdate.set(name, jepsInfo.id);
98
103
  }
99
104
 
100
105
  const efStatus = this.#getStatusCode(jepsInfo);
@@ -116,6 +121,7 @@ export class JepsonEFlora {
116
121
  this.#checkExceptions(exceptions);
117
122
 
118
123
  if (update) {
124
+ this.#updateIds(dataDir, idsToUpdate);
119
125
  this.#updateSynCSV();
120
126
  }
121
127
  }
@@ -222,7 +228,7 @@ export class JepsonEFlora {
222
228
 
223
229
  /**
224
230
  * @param {JepsonTaxon} jepsInfo
225
- * @returns {StatusCode|undefined}
231
+ * @returns {import("../taxon.js").StatusCode|undefined}
226
232
  */
227
233
  #getStatusCode(jepsInfo) {
228
234
  switch (jepsInfo.type) {
@@ -396,6 +402,24 @@ export class JepsonEFlora {
396
402
  }
397
403
  }
398
404
 
405
+ /**
406
+ * @param {string} dataDir
407
+ * @param {Map<string,string>} idsToUpdate
408
+ */
409
+ #updateIds(dataDir, idsToUpdate) {
410
+ const taxa = new TaxaCSV(dataDir);
411
+
412
+ for (const taxonData of taxa.getTaxa()) {
413
+ const id = idsToUpdate.get(taxonData.taxon_name);
414
+ if (!id) {
415
+ continue;
416
+ }
417
+ taxonData["jepson id"] = id;
418
+ }
419
+
420
+ taxa.write();
421
+ }
422
+
399
423
  #updateSynCSV() {
400
424
  const csv = new SynCSV("./data");
401
425
  const data = csv.getData();
@@ -0,0 +1,102 @@
1
+ import path from "node:path";
2
+ import { Files } from "../files.js";
3
+ import { scrape } from "@htmltools/scrape";
4
+
5
+ export class JepsonFamilies {
6
+ /**
7
+ * @param {string} toolsDataDir
8
+ * @param {string} outputdir
9
+ */
10
+ static async build(toolsDataDir, outputdir) {
11
+ const url = "https://ucjeps.berkeley.edu/eflora/toc.html";
12
+ const indexFileName = path.basename(url);
13
+ const toolsDataPath = toolsDataDir + "/jepsonfam";
14
+ const indexFilePath = toolsDataPath + "/" + indexFileName;
15
+
16
+ // Create data directory if it's not there.
17
+ Files.mkdir(toolsDataPath);
18
+
19
+ // Download the data file if it doesn't exist.
20
+ if (!Files.exists(indexFilePath)) {
21
+ console.log("retrieving Jepson family index");
22
+ await Files.fetch(url, indexFilePath);
23
+ }
24
+
25
+ const document = scrape.parseFile(indexFilePath);
26
+
27
+ const body = scrape.getSubtree(document, (t) => t.tagName === "body");
28
+ if (!body) {
29
+ throw new Error();
30
+ }
31
+ const contentDiv = scrape.getSubtree(
32
+ body,
33
+ (t) => scrape.getAttr(t, "id") === "content",
34
+ );
35
+ if (!contentDiv) {
36
+ throw new Error();
37
+ }
38
+ const rows = scrape.getSubtrees(contentDiv, (t) => t.tagName === "tr");
39
+
40
+ this.#parseRows(outputdir, rows);
41
+ }
42
+
43
+ /**
44
+ * @param {string} toolsDataPath
45
+ * @param {import("@htmltools/scrape").Element[]} rows
46
+ */
47
+ static #parseRows(toolsDataPath, rows) {
48
+ /** @type {Object<string,{section:string,id:string}>} */
49
+ const families = {};
50
+ /** @type {Object<string,{family:string,id:string}>} */
51
+ const genera = {};
52
+
53
+ for (const row of rows) {
54
+ const cols = scrape.getSubtrees(row, (t) => t.tagName === "td");
55
+ if (!cols || cols.length < 3) {
56
+ continue;
57
+ }
58
+
59
+ // Find the section.
60
+ const section = scrape.getTextContent(cols[0].children[0]);
61
+
62
+ // Find the family name and ID.
63
+ const familyLink = cols[1].children[0];
64
+ if (familyLink.type !== "element") {
65
+ throw new Error();
66
+ }
67
+ const familyTarget = scrape.getAttr(familyLink, "href");
68
+ if (!familyTarget) {
69
+ throw new Error();
70
+ }
71
+ const familyID = familyTarget.split("=")[1];
72
+ const familyName = scrape.getTextContent(familyLink.children[0]);
73
+ families[familyName] = { section: section, id: familyID };
74
+
75
+ // Find all the genera.
76
+ const genusLinks = scrape.getSubtrees(
77
+ cols[2],
78
+ (t) => t.tagName === "a",
79
+ );
80
+ for (const genusLink of genusLinks) {
81
+ const genusTarget = scrape.getAttr(genusLink, "href");
82
+ if (!genusTarget) {
83
+ throw new Error();
84
+ }
85
+ const genusID = genusTarget.split("=")[1];
86
+ const genusName = scrape.getTextContent(genusLink.children[0]);
87
+ genera[genusName] = { family: familyName, id: genusID };
88
+ }
89
+ }
90
+
91
+ Files.write(
92
+ toolsDataPath + "/families.json",
93
+ JSON.stringify(families, undefined, 4),
94
+ true,
95
+ );
96
+ Files.write(
97
+ toolsDataPath + "/genera.json",
98
+ JSON.stringify(genera, undefined, 4),
99
+ true,
100
+ );
101
+ }
102
+ }