@ca-plant-list/ca-plant-list 0.4.8 → 0.4.9
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/.github/workflows/main.yml +20 -0
- package/.vscode/settings.json +7 -2
- package/data/glossary/perianth.md +1 -0
- package/data/taxa.csv +5 -3
- package/data/text/Toxicoscordion-fremontii.md +1 -0
- package/data/text/Toxicoscordion-paniculatum.md +1 -0
- package/data/text/Toxicoscordion-venenosum-var-venenosum.md +1 -0
- package/ebook/css/main.css +9 -1
- package/eslint.config.mjs +9 -9
- package/lib/ebook/images.js +79 -52
- package/lib/ebook/pages/taxonpage.js +25 -27
- package/lib/ebook/plantbook.js +147 -139
- package/lib/htmltaxon.js +124 -117
- package/lib/inat_photo.js +15 -15
- package/lib/progressmeter.js +29 -0
- package/lib/web/pagetaxon.js +199 -190
- package/package.json +4 -3
- package/scripts/photos.js +68 -0
- package/types/classes.d.ts +14 -20
- package/data/photos.csv +0 -9
- package/lib/ebook/taxonimage.js +0 -23
@@ -0,0 +1,20 @@
|
|
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/.vscode/settings.json
CHANGED
@@ -0,0 +1 @@
|
|
1
|
+
The collection of petals and [sepals](./sepal.html) on a flower.
|
package/data/taxa.csv
CHANGED
@@ -1255,6 +1255,8 @@ Navarretia pubescens,downy pincushion,N,34471,5804,58976
|
|
1255
1255
|
Navarretia squarrosa,skunkweed,N,34475,5807,53157
|
1256
1256
|
Navarretia tagetina,,N,34477,5809,60226
|
1257
1257
|
Navarretia viscidula,sticky navarretia,N,34478,5810,78195
|
1258
|
+
Nemacladus capillaris,common threadplant,N,34503,5815,78199,annual,white,5,7
|
1259
|
+
Nemacladus montanus,mountain threadplant,N,34508,5824,78203,annual,white,5,7
|
1258
1260
|
Nemophila heterophylla,,N,34539,5834,53158,,white,2,6
|
1259
1261
|
Nemophila menziesii var. atomaria,,N,62279,5837,50647,,white,2,6
|
1260
1262
|
Nemophila menziesii var. menziesii,baby blue-eyes,N,62285,5839,50650
|
@@ -1771,9 +1773,9 @@ Torilis arvensis,tall sock-destroyer,X,46743,8004,56846
|
|
1771
1773
|
Torilis nodosa,short sock-destroyer,X,46747,8007,53304
|
1772
1774
|
Torreyochloa pallida var. pauciflora,,N,67197,8010,81431
|
1773
1775
|
Toxicodendron diversilobum,poison-oak,N,46791,8015,51080,,white
|
1774
|
-
Toxicoscordion fremontii,zigadene,N,89163,11103,49649
|
1775
|
-
Toxicoscordion paniculatum,,N,46802,11105,79380
|
1776
|
-
Toxicoscordion venenosum var. venenosum,death-camas,N,93851,11106,81432
|
1776
|
+
Toxicoscordion fremontii,zigadene,N,89163,11103,49649,bulb,white,2,6
|
1777
|
+
Toxicoscordion paniculatum,,N,46802,11105,79380,bulb,white,5,6
|
1778
|
+
Toxicoscordion venenosum var. venenosum,death-camas,N,93851,11106,81432,bulb,white,5,7
|
1777
1779
|
Tradescantia fluminensis,,X,77137,8017,79382
|
1778
1780
|
Tragopogon porrifolius,salsify,X,5449,8020,54141
|
1779
1781
|
Trianthema portulacastrum,,N,46920,8023,79390
|
@@ -0,0 +1 @@
|
|
1
|
+
Leaves 8-30 mm wide. [Perianth](./perianth.html) parts 5-15 mm long.
|
@@ -0,0 +1 @@
|
|
1
|
+
Leaves 6-16 mm wide. [Perianth](./perianth.html) parts 2-6 mm long. Perianth parts unequal, outer parts generally without claws, inner parts with claws. Stamens at least as long as perianth parts.
|
@@ -0,0 +1 @@
|
|
1
|
+
Leaves 4-10 mm wide. [Perianth](./perianth.html) parts 4-6 mm long. Stamens at least as long as perianth parts.
|
package/ebook/css/main.css
CHANGED
@@ -12,6 +12,14 @@ div.section {
|
|
12
12
|
margin-bottom: .5rem;
|
13
13
|
}
|
14
14
|
|
15
|
+
div.photos {
|
16
|
+
display: flex;
|
17
|
+
}
|
18
|
+
|
19
|
+
figure {
|
20
|
+
margin: auto;
|
21
|
+
}
|
22
|
+
|
15
23
|
img.flr-color {
|
16
24
|
padding-right: .25rem;
|
17
25
|
width: 1rem;
|
@@ -29,4 +37,4 @@ span.lcs {
|
|
29
37
|
/* Glossary */
|
30
38
|
div.glossary img {
|
31
39
|
max-height: 300px;
|
32
|
-
}
|
40
|
+
}
|
package/eslint.config.mjs
CHANGED
@@ -1,13 +1,13 @@
|
|
1
|
+
import globals from "globals";
|
2
|
+
import pluginJs from "@eslint/js";
|
3
|
+
|
4
|
+
/** @type {import('eslint').Linter.Config[]} */
|
1
5
|
export default [
|
6
|
+
{ files: ["**/*.{js,mjs,cjs}"], ignores: ["output/**"] },
|
2
7
|
{
|
3
|
-
|
4
|
-
|
5
|
-
SwitchCase: 1,
|
6
|
-
}],
|
7
|
-
|
8
|
-
"linebreak-style": ["error", "unix"],
|
9
|
-
quotes: ["error", "double"],
|
10
|
-
semi: ["error", "always"],
|
8
|
+
languageOptions: {
|
9
|
+
globals: { ...globals.browser, ...globals.node, bootstrap: false },
|
11
10
|
},
|
12
|
-
}
|
11
|
+
},
|
12
|
+
pluginJs.configs.recommended,
|
13
13
|
];
|
package/lib/ebook/images.js
CHANGED
@@ -1,20 +1,15 @@
|
|
1
1
|
import * as fs from "node:fs";
|
2
|
-
import path from "node:path";
|
3
2
|
|
4
3
|
import sharp from "sharp";
|
5
4
|
|
6
5
|
import { EBook } from "./ebook.js";
|
7
|
-
import { Config } from "../config.js";
|
8
|
-
import { CSV } from "../csv.js";
|
9
6
|
import { Files } from "../files.js";
|
10
|
-
import {
|
7
|
+
import { ProgressMeter } from "../progressmeter.js";
|
11
8
|
|
12
9
|
class Images {
|
13
10
|
#siteGenerator;
|
14
11
|
#contentDir;
|
15
12
|
#taxa;
|
16
|
-
/** @type {Object<string,TaxonImage[]>} */
|
17
|
-
#images = {};
|
18
13
|
|
19
14
|
/**
|
20
15
|
* @param {SiteGenerator} siteGenerator
|
@@ -27,53 +22,63 @@ class Images {
|
|
27
22
|
this.#taxa = taxa;
|
28
23
|
}
|
29
24
|
|
30
|
-
|
25
|
+
/**
|
26
|
+
* @param {Taxon[]} taxa
|
27
|
+
*/
|
28
|
+
async createImages(taxa) {
|
29
|
+
const meter = new ProgressMeter("processing photos", taxa.length);
|
30
|
+
|
31
|
+
const width = 300;
|
32
|
+
const quality = 40;
|
33
|
+
|
31
34
|
const photoDirSrc = "external_data/photos";
|
32
|
-
const
|
33
|
-
|
34
|
-
|
35
|
-
fs.mkdirSync(photoDirTarget, { recursive: true });
|
36
|
-
|
37
|
-
const rows = CSV.parseFile(
|
38
|
-
Config.getPackageDir() + "/data",
|
39
|
-
"photos.csv"
|
35
|
+
const photoDirCache = `external_data/photos-${width}-${quality}`;
|
36
|
+
[photoDirSrc, photoDirCache, this.#contentDir + "/i"].forEach((dir) =>
|
37
|
+
fs.mkdirSync(dir, { recursive: true }),
|
40
38
|
);
|
41
|
-
for (const row of rows) {
|
42
|
-
const name = row["taxon_name"];
|
43
|
-
const taxon = this.#taxa.getTaxon(name);
|
44
|
-
if (!taxon) {
|
45
|
-
continue;
|
46
|
-
}
|
47
39
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
40
|
+
let downloadCount = 0;
|
41
|
+
let compressCount = 0;
|
42
|
+
let copyCount = 0;
|
43
|
+
|
44
|
+
for (let index = 0; index < taxa.length; index++) {
|
45
|
+
const taxon = taxa[index];
|
46
|
+
const photos = Images.getTaxonPhotos(taxon);
|
47
|
+
for (const photo of photos) {
|
48
|
+
const ext = photo.getExt();
|
49
|
+
const cachedFileName = `${photoDirCache}/${this.getCompressedImageName(photo)}`;
|
50
|
+
|
51
|
+
if (!fs.existsSync(cachedFileName)) {
|
52
|
+
const srcFileName = `${photoDirSrc}/${photo.getId()}.${ext}`;
|
53
|
+
// Compress and cache source file.
|
54
|
+
if (!fs.existsSync(srcFileName)) {
|
55
|
+
// Retrieve original file.
|
56
|
+
if (!fs.existsSync(srcFileName)) {
|
57
|
+
// File is not there; retrieve it.
|
58
|
+
await Files.fetch(photo.getUrl(), srcFileName);
|
59
|
+
downloadCount++;
|
60
|
+
}
|
61
|
+
}
|
62
|
+
await sharp(srcFileName)
|
63
|
+
.resize({ width: width })
|
64
|
+
.jpeg({ quality: quality })
|
65
|
+
.toFile(cachedFileName);
|
66
|
+
compressCount++;
|
67
|
+
}
|
68
|
+
|
69
|
+
fs.copyFileSync(
|
70
|
+
cachedFileName,
|
71
|
+
this.getCompressedFilePath(photo),
|
72
|
+
);
|
73
|
+
copyCount++;
|
52
74
|
}
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
const prefix = src.host.includes("calflora") ? "cf-" : "inat-";
|
57
|
-
const filename = prefix + parts.slice(-1)[0] + ".jpg";
|
58
|
-
const srcFileName = photoDirSrc + "/" + filename;
|
59
|
-
const targetFileName = photoDirTarget + "/" + filename;
|
60
|
-
|
61
|
-
if (!fs.existsSync(srcFileName)) {
|
62
|
-
// File is not there; retrieve it.
|
63
|
-
console.log("retrieving " + srcFileName);
|
64
|
-
await Files.fetch(src, srcFileName);
|
65
|
-
}
|
66
|
-
|
67
|
-
await sharp(srcFileName)
|
68
|
-
.resize({ width: 300 })
|
69
|
-
.jpeg({ quality: 40 })
|
70
|
-
.toFile(targetFileName);
|
71
|
-
|
72
|
-
imageList.push(
|
73
|
-
new TaxonImage(imagePrefix + "/" + filename, row["credit"])
|
74
|
-
);
|
75
|
+
meter.update(index + 1, {
|
76
|
+
custom: ` | ${copyCount} copied - ${compressCount} compressed - ${downloadCount} downloaded`,
|
77
|
+
});
|
75
78
|
}
|
76
79
|
|
80
|
+
meter.stop();
|
81
|
+
|
77
82
|
this.#siteGenerator.copyIllustrations(this.#taxa.getFlowerColors());
|
78
83
|
}
|
79
84
|
|
@@ -88,8 +93,8 @@ class Images {
|
|
88
93
|
EBook.getManifestEntry(
|
89
94
|
"i" + index,
|
90
95
|
"i/" + fileName,
|
91
|
-
EBook.getMediaTypeForExt(ext)
|
92
|
-
)
|
96
|
+
EBook.getMediaTypeForExt(ext),
|
97
|
+
),
|
93
98
|
);
|
94
99
|
}
|
95
100
|
|
@@ -97,10 +102,32 @@ class Images {
|
|
97
102
|
}
|
98
103
|
|
99
104
|
/**
|
100
|
-
* @param {
|
105
|
+
* @param {Photo} photo
|
106
|
+
* @returns {string}
|
107
|
+
*/
|
108
|
+
getCompressedFilePath(photo) {
|
109
|
+
return `${this.#contentDir}/i/${this.getCompressedImageName(photo)}`;
|
110
|
+
}
|
111
|
+
|
112
|
+
/**
|
113
|
+
* @param {Photo} photo
|
114
|
+
* @returns {string}
|
101
115
|
*/
|
102
|
-
|
103
|
-
return
|
116
|
+
getCompressedImageName(photo) {
|
117
|
+
return `${photo.getId()}.${photo.getExt().toLowerCase()}`;
|
118
|
+
}
|
119
|
+
|
120
|
+
/**
|
121
|
+
* @param {Taxon} taxon
|
122
|
+
* @returns {Photo[]}
|
123
|
+
*/
|
124
|
+
static getTaxonPhotos(taxon) {
|
125
|
+
const photos = taxon
|
126
|
+
.getPhotos()
|
127
|
+
.filter((photo) =>
|
128
|
+
["jpg", "jpeg"].includes(photo.getExt().toLowerCase()),
|
129
|
+
);
|
130
|
+
return photos.length > 0 ? [photos[0]] : photos;
|
104
131
|
}
|
105
132
|
}
|
106
133
|
|
@@ -1,26 +1,25 @@
|
|
1
|
-
import imageSize from "image-size";
|
2
1
|
import { EBookPage } from "../ebookpage.js";
|
3
2
|
import { XHTML } from "../xhtml.js";
|
4
3
|
import { Markdown } from "../../markdown.js";
|
5
4
|
import { HTMLTaxon } from "../../htmltaxon.js";
|
6
5
|
import { Config } from "../../config.js";
|
7
6
|
import { Files } from "../../files.js";
|
7
|
+
import { Images } from "../images.js";
|
8
|
+
import imageSize from "image-size";
|
8
9
|
|
9
10
|
class TaxonPage extends EBookPage {
|
10
|
-
#outputDir;
|
11
11
|
#taxon;
|
12
|
-
#
|
12
|
+
#images;
|
13
13
|
|
14
14
|
/**
|
15
15
|
* @param {string} outputDir
|
16
16
|
* @param {Taxon} taxon
|
17
|
-
* @param {
|
17
|
+
* @param {Images} images
|
18
18
|
*/
|
19
|
-
constructor(outputDir, taxon,
|
19
|
+
constructor(outputDir, taxon, images) {
|
20
20
|
super(outputDir + "/" + taxon.getFileName(), taxon.getName());
|
21
|
-
this.#outputDir = outputDir;
|
22
21
|
this.#taxon = taxon;
|
23
|
-
this.#
|
22
|
+
this.#images = images;
|
24
23
|
}
|
25
24
|
|
26
25
|
renderPageBody() {
|
@@ -45,7 +44,7 @@ class TaxonPage extends EBookPage {
|
|
45
44
|
html += XHTML.wrap(
|
46
45
|
"div",
|
47
46
|
XHTML.getLink(family.getFileName(), family.getName()),
|
48
|
-
{ class: "section" }
|
47
|
+
{ class: "section" },
|
49
48
|
);
|
50
49
|
|
51
50
|
const cn = this.#taxon.getCommonNames();
|
@@ -59,26 +58,25 @@ class TaxonPage extends EBookPage {
|
|
59
58
|
|
60
59
|
html += renderCustomText(this.#taxon.getBaseFileName());
|
61
60
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
}
|
77
|
-
photoHTML += XHTML.wrap("figure", img);
|
78
|
-
}
|
79
|
-
if (photoHTML) {
|
80
|
-
html += XHTML.wrap("div", photoHTML);
|
61
|
+
const photos = Images.getTaxonPhotos(this.#taxon);
|
62
|
+
|
63
|
+
let photoHTML = "";
|
64
|
+
for (const photo of photos) {
|
65
|
+
const dimensions = imageSize.imageSize(
|
66
|
+
this.#images.getCompressedFilePath(photo),
|
67
|
+
);
|
68
|
+
let img = XHTML.textElement("img", "", {
|
69
|
+
src: `i/${this.#images.getCompressedImageName(photo)}`,
|
70
|
+
style: "max-width:" + dimensions.width + "px",
|
71
|
+
});
|
72
|
+
const caption = `${photo.rights === "CC0" ? "By" : "(c)"} ${photo.rightsHolder} ${photo.rights && `(${photo.rights})`}`;
|
73
|
+
if (caption) {
|
74
|
+
img += XHTML.textElement("figcaption", caption);
|
81
75
|
}
|
76
|
+
photoHTML += XHTML.wrap("figure", img);
|
77
|
+
}
|
78
|
+
if (photoHTML) {
|
79
|
+
html += XHTML.wrap("div", photoHTML, "photos");
|
82
80
|
}
|
83
81
|
|
84
82
|
return html;
|