@ca-plant-list/ca-plant-list 0.4.32 → 0.4.35

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 (63) hide show
  1. package/data/inatobsphotos.csv +503 -107
  2. package/data/inattaxonphotos.csv +679 -254
  3. package/data/synonyms.csv +82 -1
  4. package/data/taxa.csv +1976 -1903
  5. package/data/text/Allium-obtusum-var-obtusum.md +1 -0
  6. package/data/text/Allium-validum.md +1 -0
  7. package/data/text/Amelanchier-alnifolia-var-pumila.md +1 -0
  8. package/data/text/Amelanchier-utahensis.md +1 -0
  9. package/data/text/Angelica-breweri.md +1 -0
  10. package/data/text/Angelica-capitellata.md +1 -0
  11. package/data/text/Castilleja-applegatei-subsp-pallida.md +1 -0
  12. package/data/text/Castilleja-miniata-subsp-miniata.md +1 -0
  13. package/data/text/Ceanothus-cordulatus.md +1 -0
  14. package/data/text/Ceanothus-velutinus.md +1 -0
  15. package/data/text/Collinsia-parviflora.md +1 -0
  16. package/data/text/Collinsia-torreyi.md +1 -0
  17. package/data/text/Ericameria-nauseosa-var-speciosa.md +1 -0
  18. package/data/text/Ericameria-nauseosa.md +1 -0
  19. package/data/text/Erigeron-algidus.md +1 -0
  20. package/data/text/Erigeron-coulteri.md +1 -0
  21. package/data/text/Hackelia-micrantha.md +1 -0
  22. package/data/text/Linanthus-pungens-subsp-pulchriflorus.md +1 -0
  23. package/data/text/Lupinus-argenteus-var-meionanthus.md +1 -0
  24. package/data/text/Lupinus-latifolius-var-latifolius.md +1 -1
  25. package/data/text/Lupinus-polyphyllus-var-burkei.md +1 -0
  26. package/data/text/Penstemon-deustus-var-deustus.md +1 -0
  27. package/data/text/Penstemon-deustus-var-pedicellatus.md +1 -0
  28. package/data/text/Penstemon-heterodoxus-var-heterodoxus.md +1 -0
  29. package/data/text/Penstemon-rydbergii-var-oreocharis.md +1 -0
  30. package/data/text/Phlox-diffusa.md +1 -0
  31. package/data/text/Potentilla-flabellifolia.md +1 -0
  32. package/data/text/Potentilla-gracilis-var-fastigiata.md +1 -0
  33. package/data/text/Primula-jeffreyi.md +1 -1
  34. package/data/text/Primula-tetrandra.md +1 -1
  35. package/data/text/Pyrola-asarifolia-subsp-asarifolia.md +1 -0
  36. package/data/text/Pyrola-dentata.md +1 -0
  37. package/data/text/Pyrola-picta.md +1 -0
  38. package/data/text/Salix-lemmonii.md +1 -0
  39. package/data/text/Salix-scouleriana.md +1 -0
  40. package/data/text/Senecio-integerrimus-var-exaltatus.md +1 -0
  41. package/data/text/Senecio-integerrimus-var-major.md +1 -0
  42. package/data/text/Senecio-triangularis.md +1 -0
  43. package/data/text/Sorbus-californica.md +1 -0
  44. package/data/text/Sorbus-scopulina.md +1 -0
  45. package/ebook/css/main.css +8 -0
  46. package/lib/ebook/ebook.js +178 -178
  47. package/lib/ebook/ebooksitegenerator.js +2 -0
  48. package/lib/ebook/pages/taxonpage.js +19 -9
  49. package/lib/ebook/plantbook.js +17 -3
  50. package/lib/externalsites.js +12 -0
  51. package/lib/htmltaxon.js +4 -0
  52. package/lib/index.d.ts +2 -0
  53. package/lib/taxonomy/taxon.js +12 -2
  54. package/lib/tools/calflora.js +0 -2
  55. package/lib/tools/calipc.js +111 -0
  56. package/lib/tools/taxacsv.js +5 -4
  57. package/lib/utils/eleventyGenerator.js +2 -0
  58. package/lib/utils/inat-tools.js +8 -4
  59. package/lib/web/pageTaxon.js +1 -0
  60. package/package.json +1 -1
  61. package/scripts/build-ebook.js +20 -5
  62. package/scripts/cpl-photos.js +4 -4
  63. package/scripts/cpl-tools.js +15 -1
@@ -11,16 +11,19 @@ import { TaxonPage } from "./pages/taxonpage.js";
11
11
  import { TOCPage } from "./pages/tocpage.js";
12
12
 
13
13
  class PlantBook extends EBook {
14
+ #config;
14
15
  #taxa;
15
16
  #glossary;
16
17
  #images;
18
+ #maxTaxa;
17
19
 
18
20
  /**
19
21
  * @param {string} outputDir
20
22
  * @param {import("../config.js").Config} config
21
23
  * @param {import("../types.js").Taxa} taxa
24
+ * @param {number|undefined} maxTaxa
22
25
  */
23
- constructor(outputDir, config, taxa) {
26
+ constructor(outputDir, config, taxa, maxTaxa) {
24
27
  super(
25
28
  outputDir,
26
29
  getRequiredConfigValue(config, "filename"),
@@ -28,16 +31,21 @@ class PlantBook extends EBook {
28
31
  getRequiredConfigValue(config, "title"),
29
32
  );
30
33
 
34
+ this.#config = config;
31
35
  this.#taxa = taxa;
32
36
  const generator = new EBookSiteGenerator(config, this.getContentDir());
33
37
  this.#glossary = new GlossaryPages(generator);
34
38
  this.#images = new Images(generator, this.getContentDir(), taxa);
39
+ this.#maxTaxa = maxTaxa;
35
40
  }
36
41
 
37
42
  async createPages() {
38
43
  const contentDir = this.getContentDir();
39
44
 
40
- const taxonList = this.#taxa.getTaxonList();
45
+ const allTaxa = this.#taxa.getTaxonList();
46
+ const taxonList = this.#maxTaxa
47
+ ? allTaxa.slice(0, this.#maxTaxa)
48
+ : allTaxa;
41
49
 
42
50
  await this.#images.createImages(taxonList);
43
51
 
@@ -45,9 +53,15 @@ class PlantBook extends EBook {
45
53
  "creating taxon pages",
46
54
  taxonList.length,
47
55
  );
56
+
48
57
  for (let index = 0; index < taxonList.length; index++) {
49
58
  const taxon = taxonList[index];
50
- new TaxonPage(contentDir, taxon, this.#images).create();
59
+ new TaxonPage(
60
+ contentDir,
61
+ this.#config,
62
+ taxon,
63
+ this.#images,
64
+ ).create();
51
65
  meter.update(index + 1);
52
66
  }
53
67
  meter.stop();
@@ -30,6 +30,18 @@ export class ExternalSites {
30
30
  return new URL("https://www.calflora.org/app/taxon?crn=" + calfloraID);
31
31
  }
32
32
 
33
+ /**
34
+ * @param {import("./types.js").Taxon} taxon
35
+ * @returns {URL|undefined}
36
+ */
37
+ static getCalIPCRefLink(taxon) {
38
+ const calipcID = taxon.getCalIPCID();
39
+ if (!calipcID) {
40
+ return;
41
+ }
42
+ return new URL(`https://www.cal-ipc.org/plants/profile/${calipcID}/`);
43
+ }
44
+
33
45
  /**
34
46
  * @param {import("./types.js").Taxon} taxon
35
47
  * @returns {URL|undefined}
package/lib/htmltaxon.js CHANGED
@@ -74,6 +74,10 @@ const REFLINKS = {
74
74
  label: "Calflora",
75
75
  href: (taxon) => ExternalSites.getCalfloraRefLink(taxon),
76
76
  },
77
+ calipc: {
78
+ label: "Cal-IPC",
79
+ href: (taxon) => ExternalSites.getCalIPCRefLink(taxon),
80
+ },
77
81
  calscape: {
78
82
  label: "Calscape",
79
83
  href: (taxon) => ExternalSites.getCalscapeLink(taxon),
package/lib/index.d.ts CHANGED
@@ -8,6 +8,7 @@ type PhotoRights = "CC0" | "CC BY" | "CC BY-NC" | "C" | null;
8
8
 
9
9
  type RefSourceCode =
10
10
  | "calflora"
11
+ | "calipc"
11
12
  | "calscape"
12
13
  | "cch"
13
14
  | "fna"
@@ -40,6 +41,7 @@ export type TaxonData = TaxonomyData & {
40
41
  CRPR: string;
41
42
  FESA: string;
42
43
  fna: string;
44
+ calipc: string;
43
45
  flower_color: string;
44
46
  GRank: string;
45
47
  "inat id": string;
@@ -13,6 +13,7 @@ class Taxon extends Taxonomy {
13
13
  #cch2id;
14
14
  #fnaName;
15
15
  #calscapeCN;
16
+ #calipcID;
16
17
  #lifeCycle;
17
18
  #flowerColors;
18
19
  #bloomStart;
@@ -51,6 +52,7 @@ class Taxon extends Taxonomy {
51
52
  this.#iNatID = data["inat id"];
52
53
  this.#cch2id = data.cch2_id;
53
54
  this.#fnaName = data.fna ?? "";
55
+ this.#calipcID = data.calipc ?? "";
54
56
  this.#calscapeCN =
55
57
  data.calscape_cn === "" ? undefined : data.calscape_cn;
56
58
  this.#lifeCycle = data.life_cycle;
@@ -118,12 +120,20 @@ class Taxon extends Taxonomy {
118
120
  return this.#bloomStart;
119
121
  }
120
122
 
123
+ getCalfloraID() {
124
+ return this.#calRecNum;
125
+ }
126
+
121
127
  getCalfloraName() {
122
128
  return this.getName().replace(" subsp.", " ssp.").replace("×", "X");
123
129
  }
124
130
 
125
- getCalfloraID() {
126
- return this.#calRecNum;
131
+ getCalIPCID() {
132
+ return this.#calipcID;
133
+ }
134
+
135
+ getCalIPCName() {
136
+ return this.getName().replace(" subsp.", " ssp.");
127
137
  }
128
138
 
129
139
  getCalscapeCommonName() {
@@ -70,12 +70,10 @@ export class Calflora {
70
70
  );
71
71
 
72
72
  /** @type {CalfloraData[]} */
73
- // @ts-ignore
74
73
  const csvActive = CSV.readFile(
75
74
  path.join(toolsDataPath, calfloraDataFileNameActive),
76
75
  );
77
76
  /** @type {CalfloraData[]} */
78
- // @ts-ignore
79
77
  const csvCounties = CSV.readFile(
80
78
  path.join(toolsDataPath, calfloraDataFileNameCounties),
81
79
  );
@@ -0,0 +1,111 @@
1
+ import * as path from "path";
2
+ import { Files } from "../files.js";
3
+ import { TaxaCSV } from "./taxacsv.js";
4
+ import { scrape } from "@htmltools/scrape";
5
+
6
+ export class CalIPC {
7
+ /**
8
+ * @param {string} toolsDataDir
9
+ * @param {string} dataDir
10
+ * @param {import("../types.js").Taxa} taxa
11
+ * @param {import("../exceptions.js").Exceptions} exceptions
12
+ * @param {import("../errorlog.js").ErrorLog} errorLog
13
+ * @param {boolean} update
14
+ */
15
+ static async analyze(
16
+ toolsDataDir,
17
+ dataDir,
18
+ taxa,
19
+ exceptions,
20
+ errorLog,
21
+ update,
22
+ ) {
23
+ const toolsDataPath = toolsDataDir + "/calipc";
24
+ // Create data directory if it's not there.
25
+ Files.mkdir(toolsDataPath);
26
+
27
+ const calipcFileName = path.join(toolsDataPath, "inventory.html");
28
+ if (!Files.exists(calipcFileName)) {
29
+ console.info("retrieving " + calipcFileName);
30
+ await Files.fetch(
31
+ "https://www.cal-ipc.org/plants/inventory/",
32
+ calipcFileName,
33
+ );
34
+ }
35
+
36
+ const parsed = scrape.parseFile(calipcFileName);
37
+ const links = scrape.getSubtrees(parsed, (e) => {
38
+ if (e.tagName !== "td") {
39
+ return false;
40
+ }
41
+ const className = scrape.getAttr(e, "class");
42
+ return className === "it-latin";
43
+ });
44
+
45
+ /** @type {Map<string,string>} */
46
+ const calipcTaxa = new Map();
47
+ for (const link of links) {
48
+ const name = scrape.getTextContent(link);
49
+ const url = scrape.getAttr(link.children[0], "href");
50
+ if (!url) {
51
+ console.warn(`Cal-IPC url not found for ${name}`);
52
+ continue;
53
+ }
54
+ const id = url.match(/\/profile\/(.*)\//);
55
+ if (!id) {
56
+ console.warn(`Cal-IPC url mismatch for ${url}`);
57
+ continue;
58
+ }
59
+ calipcTaxa.set(name, id[1]);
60
+ }
61
+
62
+ const idsToUpdate = new Map();
63
+
64
+ for (const taxon of taxa.getTaxonList()) {
65
+ const name = taxon.getName();
66
+ if (name.includes(" unknown")) {
67
+ continue;
68
+ }
69
+ const calipcName = taxon.getCalIPCName();
70
+ const calipcData = calipcTaxa.get(calipcName);
71
+ if (!calipcData) {
72
+ continue;
73
+ }
74
+
75
+ // Check native status.
76
+ if (taxon.isCANative()) {
77
+ errorLog.log(name, "is native but listed in Cal-IPC");
78
+ }
79
+
80
+ if (calipcData !== taxon.getCalIPCID()) {
81
+ errorLog.log(
82
+ name,
83
+ "Cal-IPC ID in Cal-IPC is different than taxa.csv",
84
+ calipcData,
85
+ taxon.getCalIPCID(),
86
+ );
87
+ idsToUpdate.set(name, calipcData);
88
+ }
89
+ }
90
+
91
+ if (update) {
92
+ this.#updateIds(dataDir, idsToUpdate);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * @param {string} dataDir
98
+ * @param {Map<string,string>} idsToUpdate
99
+ */
100
+ static #updateIds(dataDir, idsToUpdate) {
101
+ const taxa = new TaxaCSV(dataDir);
102
+ for (const taxonData of taxa.getTaxa()) {
103
+ const id = idsToUpdate.get(taxonData.taxon_name);
104
+ if (!id) {
105
+ continue;
106
+ }
107
+ taxonData["calipc"] = id;
108
+ }
109
+ taxa.write();
110
+ }
111
+ }
@@ -5,16 +5,17 @@ const HEADERS = [
5
5
  "taxon_name",
6
6
  "common name",
7
7
  "status",
8
+ "life_cycle",
9
+ "flower_color",
10
+ "bloom_start",
11
+ "bloom_end",
8
12
  "jepson id",
9
13
  "calrecnum",
10
14
  "inat id",
11
15
  "cch2_id",
12
16
  "fna",
13
17
  "calscape_cn",
14
- "life_cycle",
15
- "flower_color",
16
- "bloom_start",
17
- "bloom_end",
18
+ "calipc",
18
19
  "RPI ID",
19
20
  "CRPR",
20
21
  "CESA",
@@ -66,6 +66,8 @@ export class EleventyGenerator extends SiteGenerator {
66
66
  const passThroughPatterns = [
67
67
  "assets",
68
68
  "i",
69
+ "calflora_list_*.txt",
70
+ "inat_list_*.txt",
69
71
  "errors.tsv",
70
72
  ...generator.getPassThroughPatterns(),
71
73
  ];
@@ -69,7 +69,11 @@ export function convertToCSVPhoto(apiPhoto) {
69
69
  */
70
70
  async function fetchInatTaxa(inatTaxonIDs) {
71
71
  const url = `https://api.inaturalist.org/v2/taxa/${inatTaxonIDs.join(",")}?fields=(taxon_photos:(photo:(medium_url:!t,attribution:!t,license_code:!t)))`;
72
- const resp = await fetch(url);
72
+ const resp = await getResponse(url);
73
+ if (resp instanceof Error) {
74
+ console.error(`unable to fetch taxa: ${resp.message}`);
75
+ return [];
76
+ }
73
77
  if (!resp.ok) {
74
78
  const error = await resp.text();
75
79
  throw new Error(`Failed to fetch taxa from iNat: ${error}`);
@@ -163,7 +167,7 @@ export async function getObsPhotosForTaxa(taxaToUpdate, locationOptions) {
163
167
  continue;
164
168
  }
165
169
 
166
- // Just get the CC-licensed ones, 5 per taxon should be fine (max is 20 on iNat). Whether or not
170
+ // Just get the CC-licensed ones, 5 per taxon should be fine (max is 20 on iNat).
167
171
  const rawPhotoInfo = observations
168
172
  .map((obs) =>
169
173
  obs.observation_photos.map((op) => {
@@ -182,7 +186,7 @@ export async function getObsPhotosForTaxa(taxaToUpdate, locationOptions) {
182
186
  if (!obj) {
183
187
  continue;
184
188
  }
185
- processedPhotoInfo.push(obj);
189
+ processedPhotoInfo.push({ obsId: photo.obsId.toString(), ...obj });
186
190
  }
187
191
  photos.set(taxon.getName(), processedPhotoInfo);
188
192
 
@@ -270,7 +274,7 @@ function getAttribution(rawAttribution) {
270
274
 
271
275
  let lastQueryTime = Date.now();
272
276
  /**
273
- * @param {URL} url
277
+ * @param {URL|string} url
274
278
  * @returns {Promise<Response|Error>}
275
279
  */
276
280
  async function getResponse(url) {
@@ -35,6 +35,7 @@ export class PageTaxon extends GenericPage {
35
35
  HTMLTaxon.addRefLink(links, this.#taxon, "fna");
36
36
  HTMLTaxon.addRefLink(links, this.#taxon, "cch");
37
37
  HTMLTaxon.addRefLink(links, this.#taxon, "calscape");
38
+ HTMLTaxon.addRefLink(links, this.#taxon, "calipc");
38
39
  return links;
39
40
  }
40
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ca-plant-list/ca-plant-list",
3
- "version": "0.4.32",
3
+ "version": "0.4.35",
4
4
  "description": "Tools to create files for a website listing plants in an area of California.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -8,9 +8,16 @@ import { Program } from "../lib/program.js";
8
8
  import { Taxa } from "../lib/taxonomy/taxa.js";
9
9
 
10
10
  const program = Program.getProgram();
11
- program
12
- .option("-l, --locationsdir <dir>", "directory containing location data")
13
- .action(build);
11
+ program.option(
12
+ "-l, --locationsdir <dir>",
13
+ "directory containing location data",
14
+ );
15
+ program.option(
16
+ "--location <name>",
17
+ "name of location to generate (otherwise all ebooks will be generated)",
18
+ );
19
+ program.option("--max-taxa <number>", "maximum number of taxa to include");
20
+ program.action(build);
14
21
 
15
22
  await program.parseAsync();
16
23
 
@@ -26,6 +33,11 @@ async function build(options) {
26
33
  const outputBase = options.outputdir;
27
34
  const subdirs = Files.getDirEntries(locationsDir);
28
35
  for (const subdir of subdirs) {
36
+ // If a single location was specified, ignore the others.
37
+ if (options.location && subdir !== options.location) {
38
+ continue;
39
+ }
40
+
29
41
  console.log("Generating " + subdir);
30
42
  const suffix = "/" + subdir;
31
43
  const path = locationsDir + suffix;
@@ -34,6 +46,7 @@ async function build(options) {
34
46
  outputBase + suffix,
35
47
  path,
36
48
  options.showFlowerErrors,
49
+ options.maxTaxa,
37
50
  );
38
51
  }
39
52
  }
@@ -43,6 +56,7 @@ async function build(options) {
43
56
  options.outputdir,
44
57
  options.datadir,
45
58
  options.showFlowerErrors,
59
+ options.maxTaxa,
46
60
  );
47
61
  }
48
62
  }
@@ -51,8 +65,9 @@ async function build(options) {
51
65
  * @param {string} outputDir
52
66
  * @param {string} dataDir
53
67
  * @param {boolean} showFlowerErrors
68
+ * @param {number|undefined} maxTaxa
54
69
  */
55
- async function buildBook(outputDir, dataDir, showFlowerErrors) {
70
+ async function buildBook(outputDir, dataDir, showFlowerErrors, maxTaxa) {
56
71
  const errorLog = new ErrorLog(outputDir + "/errors.tsv");
57
72
 
58
73
  const taxa = new Taxa(
@@ -62,7 +77,7 @@ async function buildBook(outputDir, dataDir, showFlowerErrors) {
62
77
  );
63
78
 
64
79
  const config = new Config(dataDir);
65
- const ebook = new PlantBook(outputDir, config, taxa);
80
+ const ebook = new PlantBook(outputDir, config, taxa, maxTaxa);
66
81
  await ebook.create();
67
82
  errorLog.write();
68
83
  }
@@ -232,7 +232,7 @@ async function checkObsPhotos(options, errorLog) {
232
232
  for (const photo of photos) {
233
233
  const obsId = photo.obsId;
234
234
  if (!obsId) {
235
- throw new Error();
235
+ throw new Error(`no obsId in ${JSON.stringify(photo)}`);
236
236
  }
237
237
  const currentPhotos = photosById.get(obsId);
238
238
  if (!currentPhotos) {
@@ -641,11 +641,11 @@ if (!isLocal) {
641
641
 
642
642
  const checkCommand = program.command("check");
643
643
  checkCommand
644
- .description("Check photo data to ensure information is current.")
644
+ .description("Check photo data to ensure information is current")
645
645
  .action((options) => check(program.opts(), options));
646
646
  if (!isLocal) {
647
- checkCommand.option("--observations", `Check ${OBS_PHOTO_FILE_NAME}.`);
648
- checkCommand.option("--taxa", `Check ${TAXON_PHOTO_FILE_NAME}.`);
647
+ checkCommand.option("--observations", `Check ${OBS_PHOTO_FILE_NAME}`);
648
+ checkCommand.option("--taxa", `Check ${TAXON_PHOTO_FILE_NAME}`);
649
649
  }
650
650
 
651
651
  program
@@ -15,9 +15,11 @@ import { SupplementalText } from "../lib/tools/supplementaltext.js";
15
15
  import { JepsonFamilies } from "../lib/tools/jepsonfamilies.js";
16
16
  import { CCH2 } from "../lib/tools/cch2.js";
17
17
  import { FNA } from "../lib/tools/fna.js";
18
+ import { CalIPC } from "../lib/tools/calipc.js";
18
19
 
19
20
  const TOOLS = {
20
21
  CALFLORA: "calflora",
22
+ CAL_IPC: "calipc",
21
23
  CALSCAPE: "calscape",
22
24
  CCH2: "cch",
23
25
  FNA: "fna",
@@ -30,6 +32,7 @@ const TOOLS = {
30
32
 
31
33
  const ALL_TOOLS = [
32
34
  TOOLS.CALFLORA,
35
+ TOOLS.CAL_IPC,
33
36
  TOOLS.CALSCAPE,
34
37
  TOOLS.CCH2,
35
38
  TOOLS.FNA,
@@ -76,6 +79,16 @@ async function build(program, options) {
76
79
  !!options.update,
77
80
  );
78
81
  break;
82
+ case TOOLS.CAL_IPC:
83
+ await CalIPC.analyze(
84
+ TOOLS_DATA_DIR,
85
+ options.datadir,
86
+ taxa,
87
+ exceptions,
88
+ errorLog,
89
+ !!options.update,
90
+ );
91
+ break;
79
92
  case TOOLS.CALSCAPE:
80
93
  await Calscape.analyze(
81
94
  TOOLS_DATA_DIR,
@@ -167,8 +180,9 @@ program.addHelpText(
167
180
  "after",
168
181
  `
169
182
  Tools:
170
- 'all' runs the 'calflora', '${TOOLS.CALSCAPE}', '${TOOLS.CCH2}, '${TOOLS.FNA}, 'inat', 'jepson-eflora', 'rpi', and 'text' tools.
183
+ 'all' runs the 'calflora', '${TOOLS.CAL_IPC}', '${TOOLS.CALSCAPE}', '${TOOLS.CCH2}, '${TOOLS.FNA}, 'inat', 'jepson-eflora', 'rpi', and 'text' tools.
171
184
  '${TOOLS.CALFLORA}' retrieves data from Calflora and compares with local data.
185
+ '${TOOLS.CAL_IPC}' retrieves data from Cal-IPC and compares with local data.
172
186
  '${TOOLS.CALSCAPE}' retrieves data from Calscape and compares with local data.
173
187
  '${TOOLS.CCH2}' retrieves data from CCH2 and compares with local data.
174
188
  '${TOOLS.FNA}' retrieves data from Flora of North America and compares with local data.