@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.
@@ -8,197 +8,206 @@ import { Markdown } from "../markdown.js";
8
8
  import { Config } from "../config.js";
9
9
 
10
10
  class PageTaxon extends GenericPage {
11
- #config;
12
- #taxon;
13
-
14
- /**
15
- * @param {string} outputDir
16
- * @param {Config} config
17
- * @param {Taxon} taxon
18
- */
19
- constructor(outputDir, config, taxon) {
20
- super(outputDir, taxon.getName(), taxon.getBaseFileName());
21
- this.#config = config;
22
- this.#taxon = taxon;
23
- }
24
-
25
- #getInfoLinks() {
26
- const links = [];
27
- const jepsonID = this.#taxon.getJepsonID();
28
- if (jepsonID) {
29
- links.push(Jepson.getEFloraLink(jepsonID));
11
+ #config;
12
+ #taxon;
13
+
14
+ /**
15
+ * @param {string} outputDir
16
+ * @param {Config} config
17
+ * @param {Taxon} taxon
18
+ */
19
+ constructor(outputDir, config, taxon) {
20
+ super(outputDir, taxon.getName(), taxon.getBaseFileName());
21
+ this.#config = config;
22
+ this.#taxon = taxon;
30
23
  }
31
- const cfLink = this.#taxon.getCalfloraTaxonLink();
32
- if (cfLink) {
33
- links.push(cfLink);
34
- }
35
- const iNatLink = this.#taxon.getINatTaxonLink();
36
- if (iNatLink) {
37
- links.push(iNatLink);
38
- }
39
- const rpiLink = this.#taxon.getRPITaxonLink();
40
- if (rpiLink) {
41
- links.push(rpiLink);
42
- }
43
- return links;
44
- }
45
-
46
- #getObsLinks() {
47
- const links = [];
48
- links.push(
49
- HTML.getLink(
50
- "https://www.calflora.org/entry/observ.html?track=m#srch=t&grezc=5&cols=b&lpcli=t&cc=" +
51
- this.#config.getCountyCodes().join("!") +
52
- "&incobs=f&taxon=" +
53
- this.#taxon.getCalfloraName().replaceAll(" ", "+"),
54
- "Calflora",
55
- {},
56
- true
57
- )
58
- );
59
- const iNatID = this.#taxon.getINatID();
60
- if (iNatID) {
61
- const projectId = links.push(
62
- HTML.getLink(
63
- ExternalSites.getInatObsLink({
64
- project_id: this.#config.getConfigValue("inat", "project_id"),
65
- subview: "map",
66
- taxon_id: iNatID,
67
- }),
68
- "iNaturalist",
69
- {},
70
- true
71
- )
72
- );
24
+
25
+ #getInfoLinks() {
26
+ const links = [];
27
+ const jepsonID = this.#taxon.getJepsonID();
28
+ if (jepsonID) {
29
+ links.push(Jepson.getEFloraLink(jepsonID));
30
+ }
31
+ const cfLink = this.#taxon.getCalfloraTaxonLink();
32
+ if (cfLink) {
33
+ links.push(cfLink);
34
+ }
35
+ const iNatLink = this.#taxon.getINatTaxonLink();
36
+ if (iNatLink) {
37
+ links.push(iNatLink);
38
+ }
39
+ const rpiLink = this.#taxon.getRPITaxonLink();
40
+ if (rpiLink) {
41
+ links.push(rpiLink);
42
+ }
43
+ return links;
73
44
  }
74
45
 
75
- return links;
76
- }
77
-
78
- /**
79
- * @param {string[]} list
80
- * @param {string} header
81
- * @param {string} className
82
- */
83
- #getListSectionHTML(list, header, className) {
84
- let html = "";
85
- if (list.length > 0) {
86
- html += '<div class="section nobullet ' + className + '">';
87
- html += HTML.textElement("h2", header);
88
- html += "<ul>";
89
- html += HTML.arrayToLI(list);
90
- html += "</ul>";
91
- html += "</div>";
46
+ #getObsLinks() {
47
+ const links = [];
48
+ links.push(
49
+ HTML.getLink(
50
+ "https://www.calflora.org/entry/observ.html?track=m#srch=t&grezc=5&cols=b&lpcli=t&cc=" +
51
+ this.#config.getCountyCodes().join("!") +
52
+ "&incobs=f&taxon=" +
53
+ this.#taxon.getCalfloraName().replaceAll(" ", "+"),
54
+ "Calflora",
55
+ {},
56
+ true,
57
+ ),
58
+ );
59
+ const iNatID = this.#taxon.getINatID();
60
+ if (iNatID) {
61
+ links.push(
62
+ HTML.getLink(
63
+ ExternalSites.getInatObsLink({
64
+ project_id: this.#config.getConfigValue(
65
+ "inat",
66
+ "project_id",
67
+ ),
68
+ subview: "map",
69
+ taxon_id: iNatID,
70
+ }),
71
+ "iNaturalist",
72
+ {},
73
+ true,
74
+ ),
75
+ );
76
+ }
77
+
78
+ return links;
92
79
  }
93
- return html;
94
- }
95
80
 
96
- #getRarityInfo() {
97
- const cnpsRank = this.#taxon.getRPIRankAndThreat();
98
- if (!cnpsRank) {
99
- return "";
81
+ /**
82
+ * @param {string[]} list
83
+ * @param {string} header
84
+ * @param {string} className
85
+ */
86
+ #getListSectionHTML(list, header, className) {
87
+ let html = "";
88
+ if (list.length > 0) {
89
+ html += '<div class="section nobullet ' + className + '">';
90
+ html += HTML.textElement("h2", header);
91
+ html += "<ul>";
92
+ html += HTML.arrayToLI(list);
93
+ html += "</ul>";
94
+ html += "</div>";
95
+ }
96
+ return html;
100
97
  }
101
- const ranks = [];
102
-
103
- ranks.push(
104
- HTML.textElement("span", "CNPS Rare Plant Rank:", {
105
- class: "label",
106
- }) + HTML.getToolTip(cnpsRank, this.#taxon.getRPIRankAndThreatTooltip())
107
- );
108
- if (this.#taxon.getCESA()) {
109
- ranks.push(
110
- HTML.textElement("span", "CESA:", { class: "label" }) +
111
- RarePlants.getCESADescription(this.#taxon.getCESA())
112
- );
98
+
99
+ #getRarityInfo() {
100
+ const cnpsRank = this.#taxon.getRPIRankAndThreat();
101
+ if (!cnpsRank) {
102
+ return "";
103
+ }
104
+ const ranks = [];
105
+
106
+ ranks.push(
107
+ HTML.textElement("span", "CNPS Rare Plant Rank:", {
108
+ class: "label",
109
+ }) +
110
+ HTML.getToolTip(
111
+ cnpsRank,
112
+ this.#taxon.getRPIRankAndThreatTooltip(),
113
+ ),
114
+ );
115
+ if (this.#taxon.getCESA()) {
116
+ ranks.push(
117
+ HTML.textElement("span", "CESA:", { class: "label" }) +
118
+ RarePlants.getCESADescription(this.#taxon.getCESA()),
119
+ );
120
+ }
121
+
122
+ return HTML.wrap("div", "<ul>" + HTML.arrayToLI(ranks) + "</ul>", {
123
+ class: "section",
124
+ });
113
125
  }
114
126
 
115
- return HTML.wrap("div", "<ul>" + HTML.arrayToLI(ranks) + "</ul>", {
116
- class: "section",
117
- });
118
- }
119
-
120
- #getRelatedTaxaLinks() {
121
- const links = [];
122
- const genus = this.#taxon.getGenus();
123
- if (genus) {
124
- const taxa = genus.getTaxa();
125
- if (taxa.length > 1) {
126
- for (const taxon of taxa) {
127
- links.push(
128
- taxon.getHTMLLink(taxon.getName() !== this.#taxon.getName())
129
- );
127
+ #getRelatedTaxaLinks() {
128
+ const links = [];
129
+ const genus = this.#taxon.getGenus();
130
+ if (genus) {
131
+ const taxa = genus.getTaxa();
132
+ if (taxa.length > 1) {
133
+ for (const taxon of taxa) {
134
+ links.push(
135
+ taxon.getHTMLLink(
136
+ taxon.getName() !== this.#taxon.getName(),
137
+ ),
138
+ );
139
+ }
140
+ }
130
141
  }
131
- }
142
+ return links;
132
143
  }
133
- return links;
134
- }
135
144
 
136
- #getSynonyms() {
137
- return this.#taxon.getSynonyms();
138
- }
145
+ #getSynonyms() {
146
+ return this.#taxon.getSynonyms();
147
+ }
139
148
 
140
- render() {
141
- let html = this.getFrontMatter();
149
+ render() {
150
+ let html = this.getFrontMatter();
142
151
 
143
- html += '<div class="wrapper">';
152
+ html += '<div class="wrapper">';
144
153
 
145
- const cn = this.#taxon.getCommonNames();
146
- if (cn.length > 0) {
147
- html += HTML.textElement("div", cn.join(", "), {
148
- class: "section common-names",
149
- });
150
- }
154
+ const cn = this.#taxon.getCommonNames();
155
+ if (cn.length > 0) {
156
+ html += HTML.textElement("div", cn.join(", "), {
157
+ class: "section common-names",
158
+ });
159
+ }
151
160
 
152
- html += HTML.textElement(
153
- "div",
154
- this.#taxon.getStatusDescription(this.#config),
155
- { class: "section native-status" }
156
- );
157
-
158
- const family = this.#taxon.getFamily();
159
- html += HTML.wrap(
160
- "div",
161
- HTML.textElement("span", "Family:", { class: "label" }) +
162
- HTML.getLink("./" + family.getFileName(), family.getName()),
163
- { class: "section" }
164
- );
165
-
166
- html += this.#getRarityInfo();
167
-
168
- html += "</div>";
169
-
170
- html += HTMLTaxon.getFlowerInfo(this.#taxon, undefined, false);
171
-
172
- html += this.getMarkdown();
173
-
174
- html += '<div class="grid borders">';
175
- html += this.#getListSectionHTML(
176
- this.#getInfoLinks(),
177
- "References",
178
- "info"
179
- );
180
- html += this.#getListSectionHTML(
181
- this.#getObsLinks(),
182
- "Observations",
183
- "obs"
184
- );
185
- html += this.#getListSectionHTML(
186
- this.#getRelatedTaxaLinks(),
187
- "Related Species",
188
- "rel-taxa"
189
- );
190
- html += this.#getListSectionHTML(
191
- this.#getSynonyms(),
192
- "Synonyms",
193
- "synonyms"
194
- );
195
- html += "</div>";
196
-
197
- const photos = this.#taxon.getPhotos( );
198
- if ( photos.length > 0 ) {
199
- let photosHtml = "";
200
- for ( const photo of photos ) {
201
- photosHtml += `
161
+ html += HTML.textElement(
162
+ "div",
163
+ this.#taxon.getStatusDescription(this.#config),
164
+ { class: "section native-status" },
165
+ );
166
+
167
+ const family = this.#taxon.getFamily();
168
+ html += HTML.wrap(
169
+ "div",
170
+ HTML.textElement("span", "Family:", { class: "label" }) +
171
+ HTML.getLink("./" + family.getFileName(), family.getName()),
172
+ { class: "section" },
173
+ );
174
+
175
+ html += this.#getRarityInfo();
176
+
177
+ html += "</div>";
178
+
179
+ html += HTMLTaxon.getFlowerInfo(this.#taxon, undefined, false);
180
+
181
+ html += this.getMarkdown();
182
+
183
+ html += '<div class="grid borders">';
184
+ html += this.#getListSectionHTML(
185
+ this.#getInfoLinks(),
186
+ "References",
187
+ "info",
188
+ );
189
+ html += this.#getListSectionHTML(
190
+ this.#getObsLinks(),
191
+ "Observations",
192
+ "obs",
193
+ );
194
+ html += this.#getListSectionHTML(
195
+ this.#getRelatedTaxaLinks(),
196
+ "Related Species",
197
+ "rel-taxa",
198
+ );
199
+ html += this.#getListSectionHTML(
200
+ this.#getSynonyms(),
201
+ "Synonyms",
202
+ "synonyms",
203
+ );
204
+ html += "</div>";
205
+
206
+ const photos = this.#taxon.getPhotos();
207
+ if (photos.length > 0) {
208
+ let photosHtml = "";
209
+ for (const photo of photos) {
210
+ photosHtml += `
202
211
  <figure class="col">
203
212
  <a href="${photo.getSourceUrl()}">
204
213
  <img
@@ -213,27 +222,27 @@ class PageTaxon extends GenericPage {
213
222
  </figcaption>
214
223
  </figure>
215
224
  `;
216
- }
217
- html += `
225
+ }
226
+ html += `
218
227
  <h2>Photos</h2>
219
228
  <div class="row">
220
229
  ${photosHtml}
221
230
  </div>
222
231
  `;
223
- }
232
+ }
224
233
 
225
- const footerTextPath =
226
- Config.getPackageDir() +
227
- "/data/text/" +
228
- this.getBaseFileName() +
229
- ".footer.md";
230
- const footerMarkdown = Markdown.fileToHTML(footerTextPath);
231
- if (footerMarkdown) {
232
- html += HTML.wrap("div", footerMarkdown, "section");
233
- }
234
+ const footerTextPath =
235
+ Config.getPackageDir() +
236
+ "/data/text/" +
237
+ this.getBaseFileName() +
238
+ ".footer.md";
239
+ const footerMarkdown = Markdown.fileToHTML(footerTextPath);
240
+ if (footerMarkdown) {
241
+ html += HTML.wrap("div", footerMarkdown, "section");
242
+ }
234
243
 
235
- this.writeFile(html);
236
- }
244
+ this.writeFile(html);
245
+ }
237
246
  }
238
247
 
239
248
  export { PageTaxon };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ca-plant-list/ca-plant-list",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "Tools to create Jekyll files for a website listing plants in an area of California.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -15,7 +15,8 @@
15
15
  "types": "./lib/index.d.ts",
16
16
  "bin": {
17
17
  "ca-plant-list": "scripts/build-site.js",
18
- "ca-plant-book": "scripts/build-ebook.js"
18
+ "ca-plant-book": "scripts/build-ebook.js",
19
+ "cpl-photos": "scripts/cpl-photos.js"
19
20
  },
20
21
  "dependencies": {
21
22
  "archiver": "^5.3.1",
@@ -25,7 +26,7 @@
25
26
  "csv-stringify": "^6.5.1",
26
27
  "image-size": "^1.1.1",
27
28
  "markdown-it": "^14.1.0",
28
- "sharp": "^0.32.1",
29
+ "sharp": "^0.33.5",
29
30
  "svgo-ll": "^5.6.0",
30
31
  "unzipper": "^0.10.11"
31
32
  },
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from "path";
4
+ import { ErrorLog } from "../lib/errorlog.js";
5
+ import { Program } from "../lib/program.js";
6
+ import { Taxa } from "../lib/taxa.js";
7
+ import { getTaxonPhotos } from "../lib/utils/inat-tools.js";
8
+ import { existsSync } from "fs";
9
+ import { CSV } from "../lib/csv.js";
10
+
11
+ const PHOTO_FILE_NAME = "inattaxonphotos.csv";
12
+
13
+ const OPT_LOADER = "loader";
14
+
15
+ const MAX_PHOTOS = 5;
16
+
17
+ /**
18
+ * @param {import("commander").OptionValues} options
19
+ */
20
+ async function addMissingPhotos(options) {
21
+ const taxaMissingPhotos = [];
22
+
23
+ const taxa = await getTaxa(options);
24
+ const errorLog = new ErrorLog(options.outputdir + "/log.tsv", true);
25
+
26
+ for (const taxon of taxa.getTaxonList()) {
27
+ const photos = taxon.getPhotos();
28
+ if (photos.length < MAX_PHOTOS) {
29
+ taxaMissingPhotos.push(taxon);
30
+ }
31
+ }
32
+
33
+ const newPhotos = await getTaxonPhotos(taxaMissingPhotos);
34
+ const currentTaxaPhotos = readPhotos();
35
+
36
+ for (const [taxonName, photos] of newPhotos) {
37
+ let currentPhotos = currentTaxaPhotos.get(taxonName);
38
+ if (!currentPhotos) {
39
+ currentPhotos = [];
40
+ currentTaxaPhotos.set(taxonName, currentPhotos);
41
+ }
42
+ for (const photo of photos) {
43
+ if (currentPhotos.length === MAX_PHOTOS) {
44
+ break;
45
+ }
46
+ if (
47
+ currentPhotos.some(
48
+ (currentPhoto) => currentPhoto.id === photo.id,
49
+ )
50
+ ) {
51
+ continue;
52
+ }
53
+ currentPhotos.push(photo);
54
+ errorLog.log("adding photo", taxonName, photo.id);
55
+ }
56
+ }
57
+
58
+ errorLog.write();
59
+
60
+ // Write updated photo file.
61
+ const headers = ["name", "id", "ext", "licenseCode", "attrName"];
62
+ /** @type {string[][]} */
63
+ const data = [];
64
+ for (const taxonName of [...currentTaxaPhotos.keys()].sort()) {
65
+ // @ts-ignore
66
+ for (const photo of currentTaxaPhotos.get(taxonName)) {
67
+ data.push([
68
+ taxonName,
69
+ photo.id,
70
+ photo.ext,
71
+ photo.licenseCode,
72
+ photo.attrName ?? "",
73
+ ]);
74
+ }
75
+ }
76
+
77
+ CSV.writeFile(`${options.outputdir}/${PHOTO_FILE_NAME}`, data, headers);
78
+ }
79
+
80
+ /**
81
+ * @param {import("commander").OptionValues} options
82
+ * @param {import("commander").OptionValues} commandOptions
83
+ */
84
+ async function checkmissing(options, commandOptions) {
85
+ const taxa = await getTaxa(options);
86
+ const errorLog = new ErrorLog(options.outputdir + "/log.tsv", true);
87
+
88
+ const minPhotos = commandOptions.minphotos;
89
+ for (const taxon of taxa.getTaxonList()) {
90
+ const photos = taxon.getPhotos();
91
+ if (
92
+ minPhotos === undefined
93
+ ? photos.length !== MAX_PHOTOS
94
+ : photos.length < minPhotos
95
+ ) {
96
+ errorLog.log(taxon.getName(), photos.length.toString());
97
+ }
98
+ }
99
+
100
+ errorLog.write();
101
+ }
102
+
103
+ /**
104
+ * @param {import("commander").OptionValues} options
105
+ * @return {Promise<Taxa>}
106
+ */
107
+ async function getTaxa(options) {
108
+ const errorLog = new ErrorLog(options.outputdir + "/errors.tsv", true);
109
+
110
+ const loader = options[OPT_LOADER];
111
+ let taxa;
112
+ if (loader) {
113
+ const taxaLoaderClass = await import("file:" + path.resolve(loader));
114
+ taxa = await taxaLoaderClass.TaxaLoader.loadTaxa(options, errorLog);
115
+ } else {
116
+ taxa = new Taxa(
117
+ Program.getIncludeList(options.datadir),
118
+ errorLog,
119
+ options.showFlowerErrors,
120
+ );
121
+ }
122
+
123
+ errorLog.write();
124
+ return taxa;
125
+ }
126
+
127
+ /**
128
+ * @returns {Map<string,InatPhotoInfo[]>}
129
+ */
130
+ function readPhotos() {
131
+ const photosFileName = `./data/${PHOTO_FILE_NAME}`;
132
+ if (!existsSync(photosFileName)) {
133
+ return new Map();
134
+ }
135
+
136
+ /** @type {Map<string,{id:string,ext:string,licenseCode:string,attrName:string}[]>} */
137
+ const taxonPhotos = new Map();
138
+
139
+ /** @type {InatCsvPhoto[]} */
140
+ const csvPhotos = CSV.readFile(photosFileName);
141
+ for (const csvPhoto of csvPhotos) {
142
+ const taxonName = csvPhoto.name;
143
+ let photos = taxonPhotos.get(taxonName);
144
+ if (!photos) {
145
+ photos = [];
146
+ taxonPhotos.set(taxonName, photos);
147
+ }
148
+ photos.push({
149
+ id: csvPhoto.id.toString(),
150
+ ext: csvPhoto.ext,
151
+ licenseCode: csvPhoto.licenseCode,
152
+ attrName: csvPhoto.attrName,
153
+ });
154
+ }
155
+
156
+ return taxonPhotos;
157
+ }
158
+
159
+ const program = Program.getProgram();
160
+ program
161
+ .command("checkmissing")
162
+ .description("List taxa with less than the maximum number of photos")
163
+ .option(
164
+ "--minphotos <number>",
165
+ "Minimum number of photos. Taxa with fewer than this number will be listed.",
166
+ )
167
+ .action((options) => checkmissing(program.opts(), options));
168
+ if (process.env.npm_package_name === "@ca-plant-list/ca-plant-list") {
169
+ // Only allow updates in ca-plant-list.
170
+ program
171
+ .command("addmissing")
172
+ .description("Add photos to taxa with fewer than the maximum")
173
+ .action(() => addMissingPhotos(program.opts()));
174
+ }
175
+ program.option(
176
+ "--loader <path>",
177
+ "The path (relative to the current directory) of the JavaScript file containing the TaxaLoader class. If not provided, the default TaxaLoader will be used.",
178
+ );
179
+ await program.parseAsync();
@@ -0,0 +1,21 @@
1
+ {
2
+ "calflora": {
3
+ "counties": [
4
+ "ALA",
5
+ "CCA"
6
+ ]
7
+ },
8
+ "inat": {
9
+ "project": "ebcnps"
10
+ },
11
+ "labels": {
12
+ "introduced": "Introduced to the East Bay",
13
+ "native": "Native to the East Bay",
14
+ "status-NC": "California native introduced to the East Bay"
15
+ },
16
+ "ebook": {
17
+ "filename": "ebplants",
18
+ "pub_id": "ebplants",
19
+ "title": "East Bay Plants"
20
+ }
21
+ }