@ca-plant-list/ca-plant-list 0.4.9 → 0.4.11
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/inattaxonphotos.csv +89 -4
- package/data/synonyms.csv +7 -0
- package/data/taxa.csv +24 -22
- package/data/text/Asclepias-californica.md +1 -0
- package/data/text/Asclepias-cordifolia.md +1 -0
- package/data/text/Asclepias-eriocarpa.md +1 -0
- package/data/text/Asclepias-speciosa.md +1 -0
- package/data/text/Calystegia-malacophylla-subsp-pedicellata.md +1 -0
- package/data/text/Calystegia-purpurata-subsp-purpurata.md +1 -0
- package/data/text/Calystegia-sepium-subsp-limnophila.md +1 -0
- package/data/text/Calystegia-silvatica-subsp-disjuncta.md +1 -0
- package/data/text/Ribes-aureum-var-gracillimum.md +1 -0
- package/data/text/Ribes-californicum-var-californicum.md +1 -0
- package/data/text/Ribes-divaricatum-var-pubiflorum.md +1 -0
- package/data/text/Ribes-malvaceum-var-malvaceum.md +1 -0
- package/data/text/Ribes-menziesii-var-menziesii.md +1 -0
- package/data/text/Ribes-quercetorum.md +1 -0
- package/data/text/Ribes-sanguineum-var-glutinosum.md +1 -0
- package/data/text/Ribes-speciosum.md +1 -0
- package/data/text/Toxicoscordion-fremontii.md +1 -1
- package/data/text/Toxicoscordion-paniculatum.md +1 -1
- package/data/text/Toxicoscordion-venenosum-var-venenosum.md +1 -1
- package/data/text/Viola-purpurea-subsp-purpurea.md +1 -0
- package/data/text/Viola-purpurea-subsp-quercetorum.md +1 -1
- package/lib/csv.js +27 -5
- package/lib/taxa.js +23 -22
- package/lib/util.js +4 -3
- package/lib/utils/inat-tools.js +90 -0
- package/package.json +2 -2
- package/scripts/cpl-photos.js +179 -0
- package/tmp/config.json +21 -0
- package/tmp/exceptions.json +93 -0
- package/tmp/families.json +790 -0
- package/tmp/genera.json +5566 -0
- package/tmp/inattaxonphotos.csv +6059 -0
- package/tmp/synonyms.csv +2141 -0
- package/tmp/taxa_include.csv +19 -0
- package/types/classes.d.ts +7 -0
- package/scripts/photos.js +0 -68
package/lib/taxa.js
CHANGED
@@ -48,7 +48,7 @@ class Taxa {
|
|
48
48
|
taxonFactory = (td, g) => new Taxon(td, g),
|
49
49
|
extraTaxa = [],
|
50
50
|
extraSynonyms = [],
|
51
|
-
includePhotos = true
|
51
|
+
includePhotos = true,
|
52
52
|
) {
|
53
53
|
this.#isSubset = inclusionList !== true;
|
54
54
|
|
@@ -68,7 +68,7 @@ class Taxa {
|
|
68
68
|
extraTaxa,
|
69
69
|
inclusionList,
|
70
70
|
taxonFactory,
|
71
|
-
showFlowerErrors
|
71
|
+
showFlowerErrors,
|
72
72
|
);
|
73
73
|
|
74
74
|
// Make sure everything in the inclusionList has been loaded.
|
@@ -79,12 +79,11 @@ class Taxa {
|
|
79
79
|
}
|
80
80
|
|
81
81
|
this.#sortedTaxa = Object.values(this.#taxa).sort((a, b) =>
|
82
|
-
a.getName().localeCompare(b.getName())
|
82
|
+
a.getName().localeCompare(b.getName()),
|
83
83
|
);
|
84
84
|
|
85
|
-
|
86
|
-
|
87
|
-
this.#loadInatPhotos( dataDir );
|
85
|
+
if (includePhotos) {
|
86
|
+
this.#loadInatPhotos(dataDir);
|
88
87
|
}
|
89
88
|
|
90
89
|
const synCSV = CSV.parseFile(dataDir, "synonyms.csv");
|
@@ -95,22 +94,24 @@ class Taxa {
|
|
95
94
|
/**
|
96
95
|
* @param {string} dataDir
|
97
96
|
*/
|
98
|
-
#loadInatPhotos(
|
97
|
+
#loadInatPhotos(dataDir) {
|
99
98
|
const photosFileName = "inattaxonphotos.csv";
|
100
|
-
if (
|
99
|
+
if (fs.existsSync(path.join(dataDir, photosFileName))) {
|
101
100
|
/** @type {InatCsvPhoto[]} */
|
102
|
-
const csvPhotos = CSV.parseFile(
|
103
|
-
for (
|
101
|
+
const csvPhotos = CSV.parseFile(dataDir, photosFileName);
|
102
|
+
for (const csvPhoto of csvPhotos) {
|
104
103
|
const taxon = this.getTaxon(csvPhoto.name);
|
105
|
-
if(!taxon) {
|
104
|
+
if (!taxon) {
|
106
105
|
continue;
|
107
106
|
}
|
108
|
-
taxon.addPhoto(
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
107
|
+
taxon.addPhoto(
|
108
|
+
new InatPhoto(
|
109
|
+
csvPhoto.id,
|
110
|
+
csvPhoto.ext,
|
111
|
+
csvPhoto.licenseCode,
|
112
|
+
csvPhoto.attrName,
|
113
|
+
),
|
114
|
+
);
|
114
115
|
}
|
115
116
|
}
|
116
117
|
}
|
@@ -121,7 +122,7 @@ class Taxa {
|
|
121
122
|
|
122
123
|
getFlowerColors() {
|
123
124
|
return Object.values(this.#flower_colors).filter(
|
124
|
-
(fc) => fc.getTaxa().length > 0
|
125
|
+
(fc) => fc.getTaxa().length > 0,
|
125
126
|
);
|
126
127
|
}
|
127
128
|
|
@@ -209,10 +210,10 @@ class Taxa {
|
|
209
210
|
const color = this.#flower_colors[colorName];
|
210
211
|
if (!color) {
|
211
212
|
throw new Error(
|
212
|
-
|
213
|
+
'flower color "' +
|
213
214
|
colorName +
|
214
|
-
"
|
215
|
-
name
|
215
|
+
'" not found for ' +
|
216
|
+
name,
|
216
217
|
);
|
217
218
|
}
|
218
219
|
color.addTaxon(taxon);
|
@@ -229,7 +230,7 @@ class Taxa {
|
|
229
230
|
) {
|
230
231
|
this.#errorLog.log(
|
231
232
|
name,
|
232
|
-
"does not have all flower info"
|
233
|
+
"does not have all flower info",
|
233
234
|
);
|
234
235
|
}
|
235
236
|
} else {
|
package/lib/util.js
CHANGED
@@ -3,8 +3,9 @@
|
|
3
3
|
* https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore?tab=readme-ov-file#_chunk
|
4
4
|
* @param {any[]} input
|
5
5
|
* @param {number} size
|
6
|
+
* @returns {any[][]}
|
6
7
|
*/
|
7
|
-
export function chunk(
|
8
|
+
export function chunk(input, size) {
|
8
9
|
return input.reduce((arr, item, idx) => {
|
9
10
|
return idx % size === 0
|
10
11
|
? [...arr, [item]]
|
@@ -15,6 +16,6 @@ export function chunk( input, size ) {
|
|
15
16
|
/**
|
16
17
|
* @param {number} time
|
17
18
|
*/
|
18
|
-
export async function sleep(
|
19
|
-
return new Promise(
|
19
|
+
export async function sleep(time) {
|
20
|
+
return new Promise((resolve) => setTimeout(resolve, time));
|
20
21
|
}
|
@@ -0,0 +1,90 @@
|
|
1
|
+
import { ProgressMeter } from "../progressmeter.js";
|
2
|
+
import { chunk, sleep } from "../util.js";
|
3
|
+
|
4
|
+
const ALLOWED_LICENSE_CODES = ["cc0", "cc-by", "cc-by-nc"];
|
5
|
+
|
6
|
+
/**
|
7
|
+
* @param {Taxon[]} taxa
|
8
|
+
* @return {Promise<InatApiTaxon[]>}
|
9
|
+
*/
|
10
|
+
async function fetchInatTaxa(taxa) {
|
11
|
+
const inatTaxonIDs = taxa.map((taxon) => taxon.getINatID()).filter(Boolean);
|
12
|
+
const url = `https://api.inaturalist.org/v2/taxa/${inatTaxonIDs.join(",")}?fields=(taxon_photos:(photo:(medium_url:!t,attribution:!t,license_code:!t)))`;
|
13
|
+
const resp = await fetch(url);
|
14
|
+
if (!resp.ok) {
|
15
|
+
const error = await resp.text();
|
16
|
+
throw new Error(`Failed to fetch taxa from iNat: ${error}`);
|
17
|
+
}
|
18
|
+
const json = await resp.json();
|
19
|
+
return json.results;
|
20
|
+
}
|
21
|
+
|
22
|
+
/**
|
23
|
+
* @param {Taxon[]} taxaToUpdate
|
24
|
+
* @returns {Promise<Map<string,InatPhotoInfo[]>>}
|
25
|
+
*/
|
26
|
+
export async function getTaxonPhotos(taxaToUpdate) {
|
27
|
+
/** @type {Map<string,string>} */
|
28
|
+
const idMap = new Map();
|
29
|
+
|
30
|
+
for (const taxon of taxaToUpdate) {
|
31
|
+
if (taxon.getINatID()) {
|
32
|
+
idMap.set(taxon.getINatID(), taxon.getName());
|
33
|
+
}
|
34
|
+
}
|
35
|
+
|
36
|
+
/** @type {Map<string,InatPhotoInfo[]>} */
|
37
|
+
const photos = new Map();
|
38
|
+
|
39
|
+
const meter = new ProgressMeter("retrieving taxa", taxaToUpdate.length);
|
40
|
+
let taxaRetrieved = 0;
|
41
|
+
|
42
|
+
for (const batch of chunk(taxaToUpdate, 30)) {
|
43
|
+
const inatTaxa = await fetchInatTaxa(batch);
|
44
|
+
for (const iNatTaxon of inatTaxa) {
|
45
|
+
const iNatTaxonPhotos = iNatTaxon.taxon_photos
|
46
|
+
.filter((tp) =>
|
47
|
+
ALLOWED_LICENSE_CODES.includes(tp.photo.license_code),
|
48
|
+
)
|
49
|
+
.slice(0, 5);
|
50
|
+
|
51
|
+
const taxonName = idMap.get(iNatTaxon.id.toString());
|
52
|
+
if (!taxonName) {
|
53
|
+
throw new Error(`iNat id ${iNatTaxon.id} not found`);
|
54
|
+
}
|
55
|
+
/** @type {InatPhotoInfo[]} */
|
56
|
+
const taxonPhotos = [];
|
57
|
+
for (const taxonPhoto of iNatTaxonPhotos) {
|
58
|
+
const ext = taxonPhoto.photo.medium_url.split(".").at(-1);
|
59
|
+
if (!ext) {
|
60
|
+
continue;
|
61
|
+
}
|
62
|
+
/** @type {InatPhotoInfo} */
|
63
|
+
const obj = {
|
64
|
+
id: taxonPhoto.photo.id.toString(),
|
65
|
+
ext: ext,
|
66
|
+
licenseCode: taxonPhoto.photo.license_code,
|
67
|
+
attrName:
|
68
|
+
// Photographers retain copyright for most CC licenses,
|
69
|
+
// except CC0, so attribution is a bit different
|
70
|
+
taxonPhoto.photo.attribution.match(
|
71
|
+
/\(c\) (.*?),/,
|
72
|
+
)?.[1] ||
|
73
|
+
taxonPhoto.photo.attribution.match(
|
74
|
+
/uploaded by (.*)/,
|
75
|
+
)?.[1],
|
76
|
+
};
|
77
|
+
|
78
|
+
taxonPhotos.push(obj);
|
79
|
+
}
|
80
|
+
photos.set(taxonName, taxonPhotos);
|
81
|
+
}
|
82
|
+
taxaRetrieved += batch.length;
|
83
|
+
meter.update(taxaRetrieved);
|
84
|
+
await sleep(1_100);
|
85
|
+
}
|
86
|
+
|
87
|
+
meter.stop();
|
88
|
+
|
89
|
+
return photos;
|
90
|
+
}
|
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.11",
|
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": {
|
@@ -16,7 +16,7 @@
|
|
16
16
|
"bin": {
|
17
17
|
"ca-plant-list": "scripts/build-site.js",
|
18
18
|
"ca-plant-book": "scripts/build-ebook.js",
|
19
|
-
"
|
19
|
+
"cpl-photos": "scripts/cpl-photos.js"
|
20
20
|
},
|
21
21
|
"dependencies": {
|
22
22
|
"archiver": "^5.3.1",
|
@@ -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();
|
package/tmp/config.json
ADDED
@@ -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
|
+
}
|
@@ -0,0 +1,93 @@
|
|
1
|
+
{
|
2
|
+
"Campanula exigua": {
|
3
|
+
"rpi": {
|
4
|
+
"translation-to-rpi": "Ravenella exigua"
|
5
|
+
}
|
6
|
+
},
|
7
|
+
"Campanula sharsmithiae": {
|
8
|
+
"rpi": {
|
9
|
+
"translation-to-rpi": "Ravenella sharsmithiae"
|
10
|
+
}
|
11
|
+
},
|
12
|
+
"Castilleja ambigua subsp. ambigua": {
|
13
|
+
"rpi": {
|
14
|
+
"translation-to-rpi": "Castilleja ambigua var. ambigua"
|
15
|
+
}
|
16
|
+
},
|
17
|
+
"Castilleja ambigua var. ambigua": {
|
18
|
+
"rpi": {
|
19
|
+
"translation": "Castilleja ambigua subsp. ambigua"
|
20
|
+
}
|
21
|
+
},
|
22
|
+
"Downingia ornatissima var. mirabilis": {
|
23
|
+
"inat": {
|
24
|
+
"notintaxondata": true
|
25
|
+
}
|
26
|
+
},
|
27
|
+
"Erysimum capitatum var. angustatum": {
|
28
|
+
"calflora": {
|
29
|
+
"badjepsonid": true
|
30
|
+
},
|
31
|
+
"jepson": {
|
32
|
+
"allowsynonym": true
|
33
|
+
}
|
34
|
+
},
|
35
|
+
"Heterotheca oregona var. rudis": {
|
36
|
+
"inat": {
|
37
|
+
"notintaxondata": true
|
38
|
+
}
|
39
|
+
},
|
40
|
+
"Horkelia californica var. frondosa": {
|
41
|
+
"inat": {
|
42
|
+
"notintaxondata": true
|
43
|
+
}
|
44
|
+
},
|
45
|
+
"Lupinus littoralis var. variicolor": {
|
46
|
+
"inat": {
|
47
|
+
"notintaxondata": true
|
48
|
+
}
|
49
|
+
},
|
50
|
+
"Malacothamnus hallii": {
|
51
|
+
"comment": "in CNPS Rare Plant Inventory",
|
52
|
+
"calflora": {
|
53
|
+
"notintaxondata": true
|
54
|
+
},
|
55
|
+
"jepson": {
|
56
|
+
"allowsynonym": true
|
57
|
+
},
|
58
|
+
"rpi": {
|
59
|
+
"translation": "Malacothamnus arcuatus var. elmeri"
|
60
|
+
}
|
61
|
+
},
|
62
|
+
"Malacothamnus arcuatus var. elmeri": {
|
63
|
+
"comment": "in CNPS Rare Plant Inventory as Malacothamnus hallii",
|
64
|
+
"rpi": {
|
65
|
+
"translation-to-rpi": "Malacothamnus hallii"
|
66
|
+
}
|
67
|
+
},
|
68
|
+
"Myosurus minimus subsp. apus": {
|
69
|
+
"comment": "in CNPS Rare Plant Inventory",
|
70
|
+
"jepson": {
|
71
|
+
"notineflora": true
|
72
|
+
}
|
73
|
+
},
|
74
|
+
"Ravenella exigua": {
|
75
|
+
"comment": "in CNPS Rare Plant Inventory as Ravenella",
|
76
|
+
"rpi": {
|
77
|
+
"translation": "Campanula exigua"
|
78
|
+
}
|
79
|
+
},
|
80
|
+
"Ravenella sharsmithiae": {
|
81
|
+
"rpi": {
|
82
|
+
"translation": "Campanula sharsmithiae"
|
83
|
+
}
|
84
|
+
},
|
85
|
+
"Streptanthus albidus subsp. peramoenus": {
|
86
|
+
"calflora": {
|
87
|
+
"notintaxondata": true
|
88
|
+
},
|
89
|
+
"jepson": {
|
90
|
+
"allowsynonym": true
|
91
|
+
}
|
92
|
+
}
|
93
|
+
}
|