@ca-plant-list/ca-plant-list 0.4.33 → 0.4.36
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/glossary/peduncle.md +1 -1
- package/data/inatobsphotos.csv +499 -75
- package/data/inattaxonphotos.csv +728 -242
- package/data/synonyms.csv +86 -0
- package/data/taxa.csv +1985 -1905
- package/data/text/Allium-obtusum-var-obtusum.md +1 -0
- package/data/text/Allium-validum.md +1 -0
- package/data/text/Amelanchier-alnifolia-var-pumila.md +1 -0
- package/data/text/Amelanchier-utahensis.md +1 -0
- package/data/text/Angelica-breweri.md +1 -0
- package/data/text/Angelica-capitellata.md +1 -0
- package/data/text/Castilleja-applegatei-subsp-pallida.md +1 -0
- package/data/text/Castilleja-miniata-subsp-miniata.md +1 -0
- package/data/text/Ceanothus-cordulatus.md +1 -0
- package/data/text/Ceanothus-velutinus.md +1 -0
- package/data/text/Collinsia-parviflora.md +1 -0
- package/data/text/Collinsia-torreyi.md +1 -0
- package/data/text/Ericameria-nauseosa-var-speciosa.md +1 -0
- package/data/text/Ericameria-nauseosa.md +1 -0
- package/data/text/Erigeron-algidus.md +1 -0
- package/data/text/Erigeron-coulteri.md +1 -0
- package/data/text/Hackelia-micrantha.md +1 -0
- package/data/text/Hieracium-albiflorum.md +1 -0
- package/data/text/Hieracium-horridum.md +1 -0
- package/data/text/Hieracium-triste.md +1 -0
- package/data/text/Linanthus-pungens-subsp-pulchriflorus.md +1 -0
- package/data/text/Lupinus-argenteus-var-meionanthus.md +1 -0
- package/data/text/Lupinus-latifolius-var-latifolius.md +1 -1
- package/data/text/Lupinus-lepidus-var-lobbii.md +1 -0
- package/data/text/Lupinus-polyphyllus-var-burkei.md +1 -0
- package/data/text/Pedicularis-attollens.md +1 -0
- package/data/text/Pedicularis-groenlandica.md +1 -0
- package/data/text/Penstemon-deustus-var-deustus.md +1 -0
- package/data/text/Penstemon-deustus-var-pedicellatus.md +1 -0
- package/data/text/Penstemon-heterodoxus-var-heterodoxus.md +1 -0
- package/data/text/Penstemon-rydbergii-var-oreocharis.md +1 -0
- package/data/text/Phlox-diffusa.md +1 -0
- package/data/text/Potentilla-flabellifolia.md +1 -0
- package/data/text/Potentilla-gracilis-var-fastigiata.md +1 -0
- package/data/text/Primula-jeffreyi.md +1 -1
- package/data/text/Primula-tetrandra.md +1 -1
- package/data/text/Pseudognaphalium-beneolens.md +1 -0
- package/data/text/Pseudognaphalium-californicum.md +1 -0
- package/data/text/Pseudognaphalium-ramosissimum.md +1 -0
- package/data/text/Pyrola-asarifolia-subsp-asarifolia.md +1 -0
- package/data/text/Pyrola-dentata.md +1 -0
- package/data/text/Pyrola-picta.md +1 -0
- package/data/text/Salix-lemmonii.md +1 -0
- package/data/text/Salix-scouleriana.md +1 -0
- package/data/text/Senecio-integerrimus-var-exaltatus.md +1 -0
- package/data/text/Senecio-integerrimus-var-major.md +1 -0
- package/data/text/Senecio-triangularis.md +1 -0
- package/data/text/Sorbus-californica.md +1 -0
- package/data/text/Sorbus-scopulina.md +1 -0
- package/ebook/css/main.css +20 -13
- package/lib/csv.js +0 -2
- package/lib/ebook/ebook.js +178 -178
- package/lib/ebook/pages/taxonpage.js +19 -9
- package/lib/ebook/plantbook.js +17 -3
- package/lib/externalsites.js +12 -0
- package/lib/htmltaxon.js +13 -2
- package/lib/index.d.ts +3 -0
- package/lib/markdown.js +1 -1
- package/lib/program.js +0 -26
- package/lib/sitegenerator.js +4 -4
- package/lib/taxonomy/taxa.js +26 -5
- package/lib/taxonomy/taxon.js +12 -2
- package/lib/tools/calflora.js +0 -2
- package/lib/tools/calipc.js +111 -0
- package/lib/tools/taxacsv.js +5 -4
- package/lib/utils/eleventyGenerator.js +2 -0
- package/lib/utils/inat-tools.js +8 -4
- package/lib/web/pageTaxon.js +1 -0
- package/package.json +6 -6
- package/scripts/build-ebook.js +21 -6
- package/scripts/build-site.js +1 -1
- package/scripts/cpl-photos.js +1 -1
- package/scripts/cpl-tools.js +15 -1
- package/scripts/inattaxonphotos.js +1 -1
|
@@ -8,16 +8,19 @@ import { Images } from "../images.js";
|
|
|
8
8
|
import imageSize from "image-size";
|
|
9
9
|
|
|
10
10
|
class TaxonPage extends EBookPage {
|
|
11
|
+
#config;
|
|
11
12
|
#taxon;
|
|
12
13
|
#images;
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* @param {string} outputDir
|
|
17
|
+
* @param {Config} config
|
|
16
18
|
* @param {import("../../taxonomy/taxon.js").Taxon} taxon
|
|
17
19
|
* @param {Images} images
|
|
18
20
|
*/
|
|
19
|
-
constructor(outputDir, taxon, images) {
|
|
21
|
+
constructor(outputDir, config, taxon, images) {
|
|
20
22
|
super(outputDir + "/" + taxon.getFileName(), taxon.getName());
|
|
23
|
+
this.#config = config;
|
|
21
24
|
this.#taxon = taxon;
|
|
22
25
|
this.#images = images;
|
|
23
26
|
}
|
|
@@ -40,19 +43,26 @@ class TaxonPage extends EBookPage {
|
|
|
40
43
|
const name = this.#taxon.getName();
|
|
41
44
|
let html = XHTML.textElement("h1", name);
|
|
42
45
|
|
|
46
|
+
// Show header with common name, native status, and family.
|
|
47
|
+
let header = "";
|
|
48
|
+
|
|
49
|
+
const cn = this.#taxon.getCommonNames();
|
|
50
|
+
if (cn && cn.length > 0) {
|
|
51
|
+
header += XHTML.textElement("div", cn.join(", "));
|
|
52
|
+
}
|
|
53
|
+
|
|
43
54
|
const family = this.#taxon.getFamily();
|
|
44
|
-
|
|
55
|
+
header += XHTML.wrap(
|
|
45
56
|
"div",
|
|
46
57
|
XHTML.getLink(family.getFileName(), family.getName()),
|
|
47
|
-
{ class: "section" },
|
|
48
58
|
);
|
|
49
59
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
60
|
+
header += XHTML.textElement(
|
|
61
|
+
"div",
|
|
62
|
+
this.#taxon.getStatusDescription(this.#config),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
html += XHTML.wrap("div", header, "section hdr");
|
|
56
66
|
|
|
57
67
|
html += HTMLTaxon.getFlowerInfo(this.#taxon, "section flr");
|
|
58
68
|
|
package/lib/ebook/plantbook.js
CHANGED
|
@@ -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
|
|
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(
|
|
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();
|
package/lib/externalsites.js
CHANGED
|
@@ -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),
|
|
@@ -248,12 +252,19 @@ class HTMLTaxon {
|
|
|
248
252
|
* @returns {string}
|
|
249
253
|
*/
|
|
250
254
|
static getFooterHTML(taxon) {
|
|
251
|
-
const
|
|
255
|
+
const footerTextPathCommon = path.join(
|
|
252
256
|
Config.getPackageDir(),
|
|
253
257
|
"/data/text/",
|
|
254
258
|
`${taxon.getBaseFileName()}.footer.md`,
|
|
255
259
|
);
|
|
256
|
-
|
|
260
|
+
const footerTextPathLocal = path.join(
|
|
261
|
+
"./data/footers/",
|
|
262
|
+
`${taxon.getBaseFileName()}.md`,
|
|
263
|
+
);
|
|
264
|
+
return (
|
|
265
|
+
HTMLFragments.getMarkdownSection(footerTextPathCommon) +
|
|
266
|
+
HTMLFragments.getMarkdownSection(footerTextPathLocal)
|
|
267
|
+
);
|
|
257
268
|
}
|
|
258
269
|
|
|
259
270
|
/**
|
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;
|
|
@@ -229,6 +231,7 @@ export class Taxa<T> {
|
|
|
229
231
|
extraTaxa?: TaxonOverrides[],
|
|
230
232
|
extraSynonyms?: Record<string, string>[],
|
|
231
233
|
);
|
|
234
|
+
static getIncludeList(dataDir: string): true | Record<string, TaxonData>;
|
|
232
235
|
getTaxon(name: string): T;
|
|
233
236
|
getTaxonList(): T[];
|
|
234
237
|
}
|
package/lib/markdown.js
CHANGED
package/lib/program.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import { Files } from "./files.js";
|
|
3
|
-
import { CSV } from "./csv.js";
|
|
4
|
-
import path from "node:path";
|
|
5
2
|
|
|
6
3
|
/**
|
|
7
4
|
* @typedef {{
|
|
@@ -12,29 +9,6 @@ import path from "node:path";
|
|
|
12
9
|
*/
|
|
13
10
|
|
|
14
11
|
class Program {
|
|
15
|
-
/**
|
|
16
|
-
* @param {string} dataDir
|
|
17
|
-
*/
|
|
18
|
-
static getIncludeList(dataDir) {
|
|
19
|
-
// Read inclusion list.
|
|
20
|
-
const includeFileName = "taxa_include.csv";
|
|
21
|
-
const includeFilePath = dataDir + "/" + includeFileName;
|
|
22
|
-
if (!Files.exists(includeFilePath)) {
|
|
23
|
-
console.log(includeFilePath + " not found; loading all taxa");
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** @type {import("./index.js").TaxonData[]} */
|
|
28
|
-
// @ts-ignore
|
|
29
|
-
const includeCSV = CSV.readFile(path.join(dataDir, includeFileName));
|
|
30
|
-
/** @type {Object<string,import("./index.js").TaxonData>} */
|
|
31
|
-
const include = {};
|
|
32
|
-
for (const row of includeCSV) {
|
|
33
|
-
include[row["taxon_name"]] = row;
|
|
34
|
-
}
|
|
35
|
-
return include;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
12
|
static getProgram() {
|
|
39
13
|
const program = new Command();
|
|
40
14
|
program
|
package/lib/sitegenerator.js
CHANGED
|
@@ -56,16 +56,16 @@ export class SiteGenerator {
|
|
|
56
56
|
Config.getPackageDir() + "/data/illustrations/inkscape";
|
|
57
57
|
const entries = Files.getDirEntries(srcDir);
|
|
58
58
|
for (const entry of entries) {
|
|
59
|
-
const srcFile =
|
|
59
|
+
const srcFile = path.join(srcDir, entry);
|
|
60
60
|
const srcSVG = Files.read(srcFile);
|
|
61
61
|
const result = optimize(srcSVG, {
|
|
62
|
-
|
|
62
|
+
enable: ["removeDimensions"],
|
|
63
63
|
});
|
|
64
|
-
Files.write(
|
|
64
|
+
Files.write(path.join(outputDir, entry), result.data);
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
const outputDir =
|
|
68
|
+
const outputDir = path.join(this.#baseDir, "i");
|
|
69
69
|
Files.mkdir(outputDir);
|
|
70
70
|
|
|
71
71
|
optimizeSVG(outputDir);
|
package/lib/taxonomy/taxa.js
CHANGED
|
@@ -9,8 +9,8 @@ import { Families } from "./families.js";
|
|
|
9
9
|
import { FlowerColor } from "../flowercolor.js";
|
|
10
10
|
import { TaxaCSV } from "../tools/taxacsv.js";
|
|
11
11
|
import { ErrorLog } from "../errorlog.js";
|
|
12
|
-
import { Program } from "../program.js";
|
|
13
12
|
import { Photo } from "../photo.js";
|
|
13
|
+
import { Files } from "../files.js";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* @typedef {{Current: string;Former: string;Type: string;}} SynonymData
|
|
@@ -28,7 +28,7 @@ const FLOWER_COLORS = [
|
|
|
28
28
|
{ name: "brown", color: "brown" },
|
|
29
29
|
];
|
|
30
30
|
|
|
31
|
-
class Taxa {
|
|
31
|
+
export class Taxa {
|
|
32
32
|
#families;
|
|
33
33
|
#errorLog;
|
|
34
34
|
/** @type {Object<string,Taxon>} */
|
|
@@ -152,6 +152,29 @@ class Taxa {
|
|
|
152
152
|
);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* @param {string} dataDir
|
|
157
|
+
* @returns {true|Object<string,import("../index.js").TaxonData>}
|
|
158
|
+
*/
|
|
159
|
+
static getIncludeList(dataDir) {
|
|
160
|
+
// Read inclusion list.
|
|
161
|
+
const includeFileName = "taxa_include.csv";
|
|
162
|
+
const includeFilePath = dataDir + "/" + includeFileName;
|
|
163
|
+
if (!Files.exists(includeFilePath)) {
|
|
164
|
+
console.log(includeFilePath + " not found; loading all taxa");
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** @type {import("../index.js").TaxonData[]} */
|
|
169
|
+
const includeCSV = CSV.readFile(path.join(dataDir, includeFileName));
|
|
170
|
+
/** @type {Object<string,import("../index.js").TaxonData>} */
|
|
171
|
+
const include = {};
|
|
172
|
+
for (const row of includeCSV) {
|
|
173
|
+
include[row["taxon_name"]] = row;
|
|
174
|
+
}
|
|
175
|
+
return include;
|
|
176
|
+
}
|
|
177
|
+
|
|
155
178
|
/**
|
|
156
179
|
* @param {string} name
|
|
157
180
|
*/
|
|
@@ -193,7 +216,7 @@ class Taxa {
|
|
|
193
216
|
taxa = await taxaLoaderClass.TaxaLoader.loadTaxa(options, errorLog);
|
|
194
217
|
} else {
|
|
195
218
|
taxa = new Taxa(
|
|
196
|
-
|
|
219
|
+
Taxa.getIncludeList(options.datadir),
|
|
197
220
|
errorLog,
|
|
198
221
|
options.showFlowerErrors,
|
|
199
222
|
);
|
|
@@ -297,5 +320,3 @@ class Taxa {
|
|
|
297
320
|
}
|
|
298
321
|
}
|
|
299
322
|
}
|
|
300
|
-
|
|
301
|
-
export { Taxa };
|
package/lib/taxonomy/taxon.js
CHANGED
|
@@ -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
|
-
|
|
126
|
-
return this.#
|
|
131
|
+
getCalIPCID() {
|
|
132
|
+
return this.#calipcID;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
getCalIPCName() {
|
|
136
|
+
return this.getName().replace(" subsp.", " ssp.");
|
|
127
137
|
}
|
|
128
138
|
|
|
129
139
|
getCalscapeCommonName() {
|
package/lib/tools/calflora.js
CHANGED
|
@@ -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
|
+
}
|
package/lib/tools/taxacsv.js
CHANGED
|
@@ -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
|
-
"
|
|
15
|
-
"flower_color",
|
|
16
|
-
"bloom_start",
|
|
17
|
-
"bloom_end",
|
|
18
|
+
"calipc",
|
|
18
19
|
"RPI ID",
|
|
19
20
|
"CRPR",
|
|
20
21
|
"CESA",
|
package/lib/utils/inat-tools.js
CHANGED
|
@@ -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
|
|
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).
|
|
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) {
|
package/lib/web/pageTaxon.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.4.36",
|
|
4
4
|
"description": "Tools to create files for a website listing plants in an area of California.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -40,14 +40,14 @@
|
|
|
40
40
|
"@htmltools/scrape": "^0.1.1",
|
|
41
41
|
"archiver": "^5.3.1",
|
|
42
42
|
"cli-progress": "^3.12.0",
|
|
43
|
-
"commander": "^
|
|
43
|
+
"commander": "^14.0.1",
|
|
44
44
|
"csv-parse": "^5.6.0",
|
|
45
45
|
"csv-stringify": "^6.5.2",
|
|
46
46
|
"exceljs": "^4.4.0",
|
|
47
47
|
"image-size": "^1.1.1",
|
|
48
48
|
"markdown-it": "^14.1.0",
|
|
49
49
|
"sharp": "^0.33.5",
|
|
50
|
-
"svgo-ll": "^
|
|
50
|
+
"svgo-ll": "^6.0.1",
|
|
51
51
|
"unzipper": "^0.12.3"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
@@ -57,10 +57,10 @@
|
|
|
57
57
|
"@types/markdown-it": "^14.1.2",
|
|
58
58
|
"@types/node": "^22.10.7",
|
|
59
59
|
"@types/unzipper": "^0.10.9",
|
|
60
|
-
"eslint": "^9.
|
|
60
|
+
"eslint": "^9.35.0",
|
|
61
61
|
"jest": "^29.7.0",
|
|
62
|
-
"prettier": "^3.
|
|
62
|
+
"prettier": "^3.6.2",
|
|
63
63
|
"puppeteer": "^24.1.1",
|
|
64
|
-
"typescript": "^5.
|
|
64
|
+
"typescript": "^5.9.2"
|
|
65
65
|
}
|
|
66
66
|
}
|
package/scripts/build-ebook.js
CHANGED
|
@@ -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
|
-
|
|
13
|
-
|
|
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,18 +65,19 @@ 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(
|
|
59
|
-
|
|
74
|
+
Taxa.getIncludeList(dataDir),
|
|
60
75
|
errorLog,
|
|
61
76
|
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
|
}
|