imerss-bioinfo 1.0.0
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/README.md +156 -0
- package/build.js +196 -0
- package/docs/css/imerss-viz-all.css +1227 -0
- package/docs/css/imerss-viz-wp-overrides.css +70 -0
- package/docs/css/pepiowelh.css +76 -0
- package/docs/css/xetthecum-external.css +47 -0
- package/docs/css/xetthecum-shared.css +80 -0
- package/docs/css/xetthecum.css +526 -0
- package/docs/js/imerss-viz-all.js +58 -0
- package/docs/js/imerss-viz-all.js.map +1 -0
- package/docs/js/jquery.js +10881 -0
- package/index.html +81 -0
- package/index.js +7 -0
- package/indexWithJustMap.html +92 -0
- package/indexWithMap.html +79 -0
- package/indexWithMapHoweSound.html +91 -0
- package/indexWithMapPepiowelh.html +119 -0
- package/indexWithMapValdes.html +90 -0
- package/indexWithMapXetthecum.html +90 -0
- package/package.json +50 -0
- package/src/WoRMS/taxonAPI.js +13 -0
- package/src/arphify.js +662 -0
- package/src/assignNames.js +103 -0
- package/src/auxBuild/restoreJQuery.js +1 -0
- package/src/buildSource/Comprehensive Lists.html +42 -0
- package/src/buildSource/Data Paper Part I Visualisation.html +50 -0
- package/src/buildSource/Galiano Life List.html +43 -0
- package/src/buildSource/Metchosin/index.html +22 -0
- package/src/buildSource/Pe'pi'ow'elh.html +72 -0
- package/src/buildSource/Squamish Life List.html +48 -0
- package/src/buildSource/Valdes Island Biodiversity.html +64 -0
- package/src/buildSource/Xetthecum.html +40 -0
- package/src/buildSource/dataPaperSunburstAndMap.html +30 -0
- package/src/buildSource/index.html +224 -0
- package/src/client/css/imerss-viz-wp-overrides.css +70 -0
- package/src/client/css/imerss-viz.css +531 -0
- package/src/client/css/pepiowelh.css +76 -0
- package/src/client/css/xetthecum-external.css +47 -0
- package/src/client/css/xetthecum-shared.css +80 -0
- package/src/client/css/xetthecum.css +526 -0
- package/src/client/html/imerss-viz-map-only.html +75 -0
- package/src/client/html/imerss-viz-map-pepi.html +77 -0
- package/src/client/html/imerss-viz-map-xetthecum.html +90 -0
- package/src/client/html/imerss-viz-map.html +75 -0
- package/src/client/html/imerss-viz.html +58 -0
- package/src/client/html/importing Xetthecum patterns.txt +10 -0
- package/src/client/html/xetthecum-patterns.html +27 -0
- package/src/client/img/Animalia_2.svg +1 -0
- package/src/client/img/Animalia_Annelida.svg +655 -0
- package/src/client/img/Animalia_Chaetognatha.svg +72 -0
- package/src/client/img/Animalia_Cnidaria.svg +173 -0
- package/src/client/img/Animalia_Ctenophora.svg +298 -0
- package/src/client/img/Animalia_Nemertea.svg +54 -0
- package/src/client/img/Animalia_Phoronida.svg +271 -0
- package/src/client/img/Animalia_Platyhelminthes.svg +58 -0
- package/src/client/img/Animalia_Porifera.svg +220 -0
- package/src/client/img/Animalia_Sipuncula.svg +155 -0
- package/src/client/img/Arachnoidiscus_schmidtii_web_1-2-scaled.webp +0 -0
- package/src/client/img/Arrow-down.svg +11 -0
- package/src/client/img/Arrow-up.svg +6 -0
- package/src/client/img/Chromista.svg +1 -0
- package/src/client/img/Pepiowelh/BlackNoWe2-wb-bb.png +0 -0
- package/src/client/img/Pepiowelh/BlackNoWe2-wb.png +0 -0
- package/src/client/img/Pepiowelh/BlackNoWe2.png +0 -0
- package/src/client/img/Pepiowelh/Ecocultural Mapping Banner.svg +1 -0
- package/src/client/img/Plankton.jpg +0 -0
- package/src/client/img/Plantae.svg +1 -0
- package/src/client/img/Plantae_2.svg +1 -0
- package/src/client/img/Protista.svg +1 -0
- package/src/client/img/Valdes/Valdes Island Conservancy.jpg +0 -0
- package/src/client/img/Xetthecum/Agricultural.jpg +0 -0
- package/src/client/img/Xetthecum/Anthropogenic_banner.jpg +0 -0
- package/src/client/img/Xetthecum/Broad_Leaf.jpg +0 -0
- package/src/client/img/Xetthecum/Caves.jpg +0 -0
- package/src/client/img/Xetthecum/Coastal_Bluffs.jpg +0 -0
- package/src/client/img/Xetthecum/Conifer_Pole_Sapling.jpg +0 -0
- package/src/client/img/Xetthecum/Developed.jpg +0 -0
- package/src/client/img/Xetthecum/Dock.jpg +0 -0
- package/src/client/img/Xetthecum/Eelgrass.jpg +0 -0
- package/src/client/img/Xetthecum/Forests_banner.jpg +0 -0
- package/src/client/img/Xetthecum/Freshwater_banner.jpg +0 -0
- package/src/client/img/Xetthecum/Greig_Creek.jpg +0 -0
- package/src/client/img/Xetthecum/Intertidal.jpg +0 -0
- package/src/client/img/Xetthecum/Laughlin_Lake.jpg +0 -0
- package/src/client/img/Xetthecum/Marine_banner.jpg +0 -0
- package/src/client/img/Xetthecum/Mature_Conifer_Forest.jpg +0 -0
- package/src/client/img/Xetthecum/Porlier_Pass_Road.jpg +0 -0
- package/src/client/img/Xetthecum/Riparian.jpg +0 -0
- package/src/client/img/Xetthecum/Rural.jpg +0 -0
- package/src/client/img/Xetthecum/Subtidal.jpg +0 -0
- package/src/client/img/Xetthecum/Wetland.jpg +0 -0
- package/src/client/img/Xetthecum/Woodland.jpg +0 -0
- package/src/client/img/Xetthecum/Woodland_banner.jpg +0 -0
- package/src/client/img/Xetthecum/Xetthecum.jpg +0 -0
- package/src/client/img/Xetthecum/Young_Conifer_Forest.jpg +0 -0
- package/src/client/img/Xetthecum/Young_Mixed_Forest.jpg +0 -0
- package/src/client/img/Xetthecum/food.svg +38 -0
- package/src/client/img/Xetthecum/indicator.svg +52 -0
- package/src/client/img/Xetthecum/material.svg +186 -0
- package/src/client/img/Xetthecum/medicinal.svg +62 -0
- package/src/client/img/Xetthecum/missing-value.svg +3 -0
- package/src/client/img/Xetthecum/spiritual.svg +13 -0
- package/src/client/img/Xetthecum/trade.svg +74 -0
- package/src/client/img/audio.svg +5 -0
- package/src/client/img/external-link.svg +3 -0
- package/src/client/img/selected-eye.svg +4 -0
- package/src/client/img/selected-tick.svg +3 -0
- package/src/client/img/thumbs/BCLeps.jpg +0 -0
- package/src/client/img/thumbs/Checklist.jpg +0 -0
- package/src/client/img/thumbs/Data-Paper-Figma.jpg +0 -0
- package/src/client/img/thumbs/Galiano-Data-Paper.jpg +0 -0
- package/src/client/img/thumbs/Galiano.jpg +0 -0
- package/src/client/img/thumbs/Maxwell.jpg +0 -0
- package/src/client/img/thumbs/Squamish.jpg +0 -0
- package/src/client/img/thumbs/Valdes.jpg +0 -0
- package/src/client/img/thumbs/Xetthecum.jpg +0 -0
- package/src/client/img/up-arrow.svg +4 -0
- package/src/client/js/autocomplete.js +53 -0
- package/src/client/js/checklist.js +179 -0
- package/src/client/js/colour.js +139 -0
- package/src/client/js/datasetControls.js +212 -0
- package/src/client/js/imerss-viz.js +1397 -0
- package/src/client/js/leafletMap.js +357 -0
- package/src/client/js/leafletMapWithGrid.js +337 -0
- package/src/client/js/leafletMapWithRegions.js +381 -0
- package/src/client/js/renderSVG.js +108 -0
- package/src/client/js/tabs.js +53 -0
- package/src/client/js/xetthecum.js +106 -0
- package/src/client/json/emptyPhyloMap.json +1 -0
- package/src/client/json/kingdomPhyloMap.json +17 -0
- package/src/client/json/phyloMap.json +58 -0
- package/src/compare.js +97 -0
- package/src/compilecsv.js +47 -0
- package/src/dataProcessing/LZ4.js +27 -0
- package/src/dataProcessing/coordinatePatch.js +151 -0
- package/src/dataProcessing/readCSV.js +107 -0
- package/src/dataProcessing/readCSVwithMap.js +90 -0
- package/src/dataProcessing/readCSVwithoutMap.js +25 -0
- package/src/dataProcessing/readJSON.js +23 -0
- package/src/dataProcessing/sqlite.js +174 -0
- package/src/dataProcessing/summarise.js +122 -0
- package/src/dataProcessing/taxonScrape.js +14 -0
- package/src/dataProcessing/worker.js +31 -0
- package/src/dataProcessing/writeCSV.js +34 -0
- package/src/dataProcessing/writeJSON.js +15 -0
- package/src/dbtaxa.js +96 -0
- package/src/deAuthorise.js +48 -0
- package/src/deqgis.js +148 -0
- package/src/expressions/expr.js +65 -0
- package/src/expressions/jsep.js +925 -0
- package/src/filterFirst.js +51 -0
- package/src/geom/geoJSON.js +121 -0
- package/src/iNaturalist/examples/api.inaturalist.org-v1-taxa!q=Acmispon americanus americanus.json +200 -0
- package/src/iNaturalist/examples/api.inaturalist.org-v1-taxa!q=Flabellina japonica.json +61 -0
- package/src/iNaturalist/examples/www.inaturalist.org-taxa-877359.json +132 -0
- package/src/iNaturalist/iNatUrls.js +17 -0
- package/src/iNaturalist/obsAPI.js +150 -0
- package/src/iNaturalist/taxonAPI.js +460 -0
- package/src/inatObs.js +120 -0
- package/src/inattify.js +123 -0
- package/src/intersect.js +80 -0
- package/src/intersectFilter.js +48 -0
- package/src/landlocked.js +137 -0
- package/src/lib/jquery.binarytransport.js +51 -0
- package/src/lib/lz4-commentary.txt +9 -0
- package/src/lib/lz4.js +73 -0
- package/src/lib/point-in-polygon.js +36 -0
- package/src/lz4.js +40 -0
- package/src/marmalise.js +333 -0
- package/src/materialise.js +68 -0
- package/src/reintegrate.js +71 -0
- package/src/taxonomise.js +615 -0
- package/src/unscrewGBIF.js +44 -0
- package/src/utils/dataSource.js +173 -0
- package/src/utils/settleStructure.js +62 -0
- package/src/utils/utils.js +92 -0
- package/src/viz/plethify.js +39 -0
- package/src/wormify.js +154 -0
- package/src/xetthecum/xetthecumFilters.js +126 -0
- package/test/testDataSource.js +182 -0
- package/test/testiNatObsSource.js +38 -0
- package/test/testiNatSource.js +50 -0
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
/* eslint-env node */
|
|
2
|
+
/* eslint dot-notation: "off"*/
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const fluid = require("infusion");
|
|
6
|
+
|
|
7
|
+
fluid.require("%imerss-bioinfo");
|
|
8
|
+
|
|
9
|
+
const minimist = require("minimist");
|
|
10
|
+
|
|
11
|
+
require("./dataProcessing/readJSON.js");
|
|
12
|
+
require("./dataProcessing/writeJSON.js");
|
|
13
|
+
require("./dataProcessing/readCSV.js");
|
|
14
|
+
require("./dataProcessing/readCSVwithMap.js");
|
|
15
|
+
require("./dataProcessing/writeCSV.js");
|
|
16
|
+
require("./dataProcessing/summarise.js");
|
|
17
|
+
require("./dataProcessing/coordinatePatch.js");
|
|
18
|
+
require("./geom/geoJSON.js");
|
|
19
|
+
require("./utils/utils.js");
|
|
20
|
+
require("./utils/settleStructure.js");
|
|
21
|
+
require("./iNaturalist/taxonAPI.js");
|
|
22
|
+
|
|
23
|
+
require("./xetthecum/xetthecumFilters.js"); // TODO: Bundle into open pipeline system when we can move to it
|
|
24
|
+
|
|
25
|
+
fluid.setLogging(true);
|
|
26
|
+
fluid.defeatLogging = true;
|
|
27
|
+
|
|
28
|
+
const hortis = fluid.registerNamespace("hortis");
|
|
29
|
+
|
|
30
|
+
let jwt;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
jwt = hortis.readJSONSync("jwt.json", "reading JWT token file");
|
|
34
|
+
} catch (e) {
|
|
35
|
+
console.log(e);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const iNatTaxonSource = hortis.iNatTaxonSource({
|
|
39
|
+
jwt: jwt
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const parsedArgs = minimist(process.argv.slice(2));
|
|
43
|
+
|
|
44
|
+
const taxonResolveMap = hortis.readJSONSync("data/TaxonResolution-map.json", "reading taxon resolution map");
|
|
45
|
+
const swaps = hortis.readJSONSync("data/taxon-swaps.json5", "reading taxon swaps file");
|
|
46
|
+
if (!parsedArgs.fusion) {
|
|
47
|
+
fluid.fail("Missing argument --fusion");
|
|
48
|
+
}
|
|
49
|
+
const fusion = hortis.readJSONSync(parsedArgs.fusion);
|
|
50
|
+
|
|
51
|
+
const discardRanksBelow = "class"; // TODO: Make this an argument
|
|
52
|
+
// var discardRanksBelow = "species";
|
|
53
|
+
const discardRanksBelowIndex = hortis.ranks.indexOf(discardRanksBelow);
|
|
54
|
+
|
|
55
|
+
hortis.combineMaps = function (maps, mutable) {
|
|
56
|
+
const extendArgs = [true, {}].concat(maps);
|
|
57
|
+
const extended = fluid.extend.apply(null, extendArgs);
|
|
58
|
+
return mutable ? extended : fluid.freezeRecursive(extended);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
hortis.baseCommonOutMap = fluid.freezeRecursive({
|
|
62
|
+
"columns": {
|
|
63
|
+
"iNaturalistTaxonName": "iNaturalist taxon name",
|
|
64
|
+
"iNaturalistTaxonId": "iNaturalist taxon ID",
|
|
65
|
+
"taxonName": "Taxon name"
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Fields which are transferred to "first" and "last" entries from observations to summaries
|
|
70
|
+
hortis.obsToSummaryFields = {
|
|
71
|
+
recordedBy: "Reported By",
|
|
72
|
+
collection: "Source",
|
|
73
|
+
placeName: "Place Name",
|
|
74
|
+
catalogueNumber: "Catalogue Number",
|
|
75
|
+
observationId: "observationId",
|
|
76
|
+
timestamp: "Date Reported"
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
hortis.obsToSummaryColumns = function (fields) {
|
|
80
|
+
const togo = {};
|
|
81
|
+
const appendWith = function (prefix) {
|
|
82
|
+
const capPrefix = hortis.capitalize(prefix);
|
|
83
|
+
return function (value, key) {
|
|
84
|
+
const newKey = prefix + hortis.capitalize(key);
|
|
85
|
+
const newValue = capPrefix + " " + value;
|
|
86
|
+
togo[newKey] = newValue;
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
fluid.each(fields, appendWith("first"));
|
|
90
|
+
fluid.each(fields, appendWith("last"));
|
|
91
|
+
return togo;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
hortis.baseSummariseCommonOutMap = hortis.combineMaps([hortis.baseCommonOutMap, {
|
|
95
|
+
"counts": {
|
|
96
|
+
"observationCount": {
|
|
97
|
+
"column": "observationCount",
|
|
98
|
+
"free": true
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"columns": {
|
|
102
|
+
"observationCount": "Observation count",
|
|
103
|
+
"coords": "Coordinates"
|
|
104
|
+
}
|
|
105
|
+
}]);
|
|
106
|
+
|
|
107
|
+
hortis.ranksToColumns = function (ranks) {
|
|
108
|
+
return fluid.transform(fluid.arrayToHash(ranks), function (troo, key) {
|
|
109
|
+
return hortis.capitalize(key);
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
hortis.commonOutMap = hortis.combineMaps([hortis.baseCommonOutMap, {
|
|
114
|
+
columns: hortis.ranksToColumns(hortis.ranks)
|
|
115
|
+
}]);
|
|
116
|
+
|
|
117
|
+
hortis.commonObsOutMap = hortis.combineMaps([hortis.commonOutMap, {
|
|
118
|
+
columns: {
|
|
119
|
+
observationId: "observationId",
|
|
120
|
+
latitude: "Latitude",
|
|
121
|
+
longitude: "Longitude"
|
|
122
|
+
}
|
|
123
|
+
}]);
|
|
124
|
+
|
|
125
|
+
hortis.summariseCommonOutMap = hortis.combineMaps([hortis.baseSummariseCommonOutMap, {
|
|
126
|
+
columns: hortis.ranksToColumns(hortis.ranks)
|
|
127
|
+
}, {
|
|
128
|
+
columns: hortis.obsToSummaryColumns(hortis.obsToSummaryFields)
|
|
129
|
+
}]);
|
|
130
|
+
|
|
131
|
+
hortis.invertSwaps = function (swaps) {
|
|
132
|
+
const invertedSwaps = {};
|
|
133
|
+
fluid.each(swaps, function (value, resolvedTaxon) {
|
|
134
|
+
const iNaturalistTaxonId = value.iNaturalistTaxonId;
|
|
135
|
+
if (!iNaturalistTaxonId) {
|
|
136
|
+
fluid.fail("Swap with name " + resolvedTaxon + " does not have iNaturalistTaxonId");
|
|
137
|
+
}
|
|
138
|
+
fluid.each(value.taxonNames, function (record, taxonName) {
|
|
139
|
+
if (record.type !== "commonName") {
|
|
140
|
+
invertedSwaps[taxonName] = iNaturalistTaxonId;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
return invertedSwaps;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const invertedSwaps = hortis.invertSwaps(swaps);
|
|
148
|
+
|
|
149
|
+
hortis.checkFilters = function (filters, mapColumns) {
|
|
150
|
+
fluid.each(filters, function (oneFilter) {
|
|
151
|
+
if (!mapColumns[oneFilter.field]) {
|
|
152
|
+
fluid.fail("Column " + oneFilter.field + " is unknown in input map file");
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
return filters || {};
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
hortis.makeObsIdGenerator = function (idField, dataset) {
|
|
159
|
+
if (idField.includes("(")) {
|
|
160
|
+
const parsed = fluid.compactStringToRec(idField, "idGenerator");
|
|
161
|
+
return function (obsRow) {
|
|
162
|
+
const expandArgs = fluid.transform(parsed.args, function (arg) {
|
|
163
|
+
return fluid.stringTemplate(arg, obsRow);
|
|
164
|
+
});
|
|
165
|
+
return fluid.invokeGlobalFunction(parsed.funcName, expandArgs);
|
|
166
|
+
};
|
|
167
|
+
} else { // It must be a simple reference
|
|
168
|
+
return function (obsRow, rowNumber) {
|
|
169
|
+
const terms = fluid.extend({}, obsRow, {rowNumber: rowNumber}, dataset);
|
|
170
|
+
return fluid.stringTemplate(idField, terms);
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
hortis.assignObsIds = function (rows, map, dataset) {
|
|
176
|
+
const observationId = dataset.observationId || map.observationId;
|
|
177
|
+
if (observationId) {
|
|
178
|
+
// Note that leafletMapWithGrid expects datasets to be resolvable from prefix so that dataset controls can be operated
|
|
179
|
+
// This implies that we need local control over allocation of these in the fusion file and not always take it from the
|
|
180
|
+
// map file
|
|
181
|
+
const idGenerator = hortis.makeObsIdGenerator(observationId, dataset);
|
|
182
|
+
rows.forEach(function (row, index) {
|
|
183
|
+
const id = idGenerator(row, index);
|
|
184
|
+
row.observationId = id;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
rows.forEach(function (row) {
|
|
188
|
+
// TODO: Source this field from hortis.summarise - implies pushing summarise through args all the way back to hortis.fusionToLoadable
|
|
189
|
+
row.collection = map.datasetId;
|
|
190
|
+
});
|
|
191
|
+
return rows;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
hortis.applyFilters = function (obsRows, filters, filterCount) {
|
|
195
|
+
const origRows = obsRows.length;
|
|
196
|
+
obsRows = obsRows.filter(function (row) {
|
|
197
|
+
const pass = Object.values(filters).reduce(function (pass, filter) {
|
|
198
|
+
const element = row[filter.field];
|
|
199
|
+
const match = element === filter.equals;
|
|
200
|
+
if (match) {
|
|
201
|
+
pass = filter.exclude ? !match : match;
|
|
202
|
+
}
|
|
203
|
+
return pass;
|
|
204
|
+
}, true);
|
|
205
|
+
return pass;
|
|
206
|
+
});
|
|
207
|
+
if (filterCount > 0) {
|
|
208
|
+
console.log("Discarded " + (origRows - obsRows.length) + " rows by applying " + hortis.pluralise(filterCount, "filter"));
|
|
209
|
+
}
|
|
210
|
+
return obsRows;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
hortis.resolvePaths = function (obj, pathKeys) {
|
|
214
|
+
pathKeys.forEach(function (pathKey) {
|
|
215
|
+
const path = obj[pathKey];
|
|
216
|
+
if (!path) {
|
|
217
|
+
fluid.fail("Dataset record ", obj, " is missing required member " + pathKey);
|
|
218
|
+
}
|
|
219
|
+
obj[pathKey] = fluid.module.resolvePath(path);
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
hortis.oneDatasetToLoadable = function (dataset, key) {
|
|
224
|
+
hortis.resolvePaths(dataset, ["map", "outMap", "input"]);
|
|
225
|
+
// TODO: Turn this into a component with transform chain elements some day
|
|
226
|
+
const map = hortis.readJSONSync(dataset.map, "reading Observations map file");
|
|
227
|
+
map.datasetId = dataset.datasetId = key;
|
|
228
|
+
if (dataset.datasetClass) {
|
|
229
|
+
map.datasetClass = dataset.datasetClass;
|
|
230
|
+
}
|
|
231
|
+
const rawInput = hortis.csvReaderWithMap({
|
|
232
|
+
inputFile: dataset.input,
|
|
233
|
+
mapColumns: map.columns,
|
|
234
|
+
templateMap: dataset.templateMap !== false
|
|
235
|
+
}).completionPromise;
|
|
236
|
+
const parsedFilters = hortis.checkFilters(dataset.filters, map.columns);
|
|
237
|
+
return {
|
|
238
|
+
rawInput: rawInput,
|
|
239
|
+
obsRows: fluid.promise.map(rawInput, function (data) {
|
|
240
|
+
const rowsWithId = hortis.assignObsIds(data.rows, map, dataset);
|
|
241
|
+
const filterCount = Object.keys(parsedFilters).length;
|
|
242
|
+
const filteredRows = hortis.applyFilters(rowsWithId, parsedFilters, filterCount);
|
|
243
|
+
if (filterCount > 0) {
|
|
244
|
+
console.log("Pre-filtered observations to list of " + filteredRows.length + " with " + filterCount + " filters");
|
|
245
|
+
}
|
|
246
|
+
return filteredRows;
|
|
247
|
+
}),
|
|
248
|
+
map: map,
|
|
249
|
+
outMap: hortis.readJSONSync(dataset.outMap, "reading Observations output map file")
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// These are not functions, but they are not components
|
|
254
|
+
fluid.defaults("hortis.pipe", {
|
|
255
|
+
gradeNames: "fluid.component",
|
|
256
|
+
loader: "fluid.identity"
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
fluid.defaults("hortis.pipe.CSVInput", {
|
|
260
|
+
gradeNames: "hortis.pipe",
|
|
261
|
+
loader: "hortis.pipe.loadCSVInputPipe"
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
fluid.defaults("hortis.pipe.JSONInput", {
|
|
265
|
+
gradeNames: "hortis.pipe",
|
|
266
|
+
loader: "hortis.pipe.loadJSONInputPipe"
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
fluid.defaults("hortis.pipe.contextInput", {
|
|
270
|
+
gradeNames: "hortis.pipe",
|
|
271
|
+
loader: "hortis.pipe.loadContextInputPipe"
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
hortis.pipe.loadCSVInputPipe = function (patch) {
|
|
275
|
+
hortis.resolvePaths(patch, ["map", "input"]);
|
|
276
|
+
const map = hortis.readJSONSync(patch.map, "reading patch map file");
|
|
277
|
+
const patchData = hortis.csvReaderWithMap({
|
|
278
|
+
inputFile: patch.input,
|
|
279
|
+
mapColumns: map.columns
|
|
280
|
+
}).completionPromise;
|
|
281
|
+
return {
|
|
282
|
+
patchData: patchData,
|
|
283
|
+
map: map
|
|
284
|
+
};
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
hortis.pipe.loadJSONInputPipe = function (patch) {
|
|
288
|
+
hortis.resolvePaths(patch, ["input"]);
|
|
289
|
+
return {
|
|
290
|
+
patchData: hortis.readJSONSync(patch.input)
|
|
291
|
+
};
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
hortis.pipe.loadContextInputPipe = function (patch) {
|
|
295
|
+
hortis.resolvePaths(patch, ["input"]);
|
|
296
|
+
fluid.loadInContext(patch.input, true);
|
|
297
|
+
return {
|
|
298
|
+
patchData: fluid.getGlobalValue(patch.globalName)
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
hortis.onePatchToLoadable = function (patch) {
|
|
303
|
+
const defaults = fluid.defaults(patch.type);
|
|
304
|
+
const loaded = defaults ? fluid.invokeGlobalFunction(defaults.loader, [patch]) : {};
|
|
305
|
+
Object.assign(patch, loaded);
|
|
306
|
+
return patch;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
hortis.fusionToLoadable = function (fusion) {
|
|
310
|
+
return {
|
|
311
|
+
datasets: fluid.transform(fusion.datasets, function (dataset, key) {
|
|
312
|
+
return hortis.oneDatasetToLoadable(dataset, key);
|
|
313
|
+
}),
|
|
314
|
+
patches: fluid.transform(fusion.patches, function (patch) {
|
|
315
|
+
return hortis.onePatchToLoadable(patch);
|
|
316
|
+
}),
|
|
317
|
+
summaryPatches: fluid.transform(fusion.summaryPatches, function (patch) {
|
|
318
|
+
return hortis.onePatchToLoadable(patch);
|
|
319
|
+
})
|
|
320
|
+
};
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// Convert a BCCSN-style resolution to metres
|
|
324
|
+
hortis.mapBCCSNResolution = function (resolution) {
|
|
325
|
+
const matches = resolution.trim().match(/([\d.]+)(km|m)$/);
|
|
326
|
+
if (!matches) {
|
|
327
|
+
console.log("Warning: BCCSN resolution value " + resolution + " was not recognised");
|
|
328
|
+
} else {
|
|
329
|
+
return matches[1] * (matches[2] === "km" ? 1000 : 1);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// Some commonly used filters - split off into a proper infrastructure
|
|
334
|
+
|
|
335
|
+
hortis.deprivatise = function (resolved) {
|
|
336
|
+
resolved.obsRows.forEach(function (row) {
|
|
337
|
+
row.latitude = row.privateLatitude || row.latitude;
|
|
338
|
+
row.longitude = row.privateLongitude || row.longitude;
|
|
339
|
+
});
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
hortis.roundCoordinates = function (resolved, patch) {
|
|
343
|
+
resolved.obsRows.forEach(function (row) {
|
|
344
|
+
row.latitude = hortis.roundDecimals(row.latitude, patch.places);
|
|
345
|
+
row.longitude = hortis.roundDecimals(row.longitude, patch.places);
|
|
346
|
+
});
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
hortis.deduplicateById = function (resolved, patch) {
|
|
350
|
+
const idField = patch.idField;
|
|
351
|
+
const usedIds = {};
|
|
352
|
+
const filteredObsRows = resolved.obsRows.filter(function (row) {
|
|
353
|
+
const id = row[idField];
|
|
354
|
+
const used = usedIds[id];
|
|
355
|
+
usedIds[id] = true;
|
|
356
|
+
return !used;
|
|
357
|
+
});
|
|
358
|
+
resolved.obsRows = filteredObsRows;
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
hortis.resolveTaxa = function (target, taxaById, taxonId, columns) {
|
|
362
|
+
let taxon = taxaById[taxonId];
|
|
363
|
+
while (taxon.parentNameUsageId) {
|
|
364
|
+
if (columns[taxon.taxonRank]) {
|
|
365
|
+
target[taxon.taxonRank] = taxon.scientificName;
|
|
366
|
+
}
|
|
367
|
+
taxon = taxaById[taxon.parentNameUsageId];
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
hortis.blankRow = function (row) {
|
|
372
|
+
return fluid.transform(row, function (/* col */) {
|
|
373
|
+
return "";
|
|
374
|
+
});
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
hortis.renderDateBound = function (value) {
|
|
378
|
+
return Number.isFinite(value) ? new Date(value).toISOString() : "";
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
hortis.doSummarise = function (outrows, summarise) {
|
|
382
|
+
const that = hortis.summarise({summarise: summarise});
|
|
383
|
+
outrows.forEach(that.storeRow);
|
|
384
|
+
that.destroy();
|
|
385
|
+
if (summarise) {
|
|
386
|
+
fluid.each(that.uniqueRows, function (row) {
|
|
387
|
+
row.coords = row.coords && JSON.stringify(row.coords);
|
|
388
|
+
row.firstTimestamp = hortis.renderDateBound(row.firstTimestamp);
|
|
389
|
+
row.lastTimestamp = hortis.renderDateBound(row.lastTimestamp);
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
return Object.values(that.uniqueRows);
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
hortis.makeTaxonomiser = function (source, options) {
|
|
396
|
+
const that = {
|
|
397
|
+
options: fluid.extend(true, {}, options),
|
|
398
|
+
source: source,
|
|
399
|
+
//taxaHash: {}, // map iNat scientificName to taxon
|
|
400
|
+
// taxaById: {}, // map iNat taxonId to taxon
|
|
401
|
+
// Initialised from observations:
|
|
402
|
+
obsIdToTaxon: [], // Map of obs id to iNat taxon
|
|
403
|
+
undetHash: {}, // Map of taxon name to undetermined taxon
|
|
404
|
+
undetKeys: [], // Keys of undetHash
|
|
405
|
+
discardedTaxa: {} // Map of discarded taxon name to taxon for filtering by identification level
|
|
406
|
+
};
|
|
407
|
+
/* data.taxa.rows.forEach(function (taxon) {
|
|
408
|
+
that.taxaHash[taxon.scientificName] = taxon;
|
|
409
|
+
that.taxaById[taxon.taxonId] = taxon;
|
|
410
|
+
});*/
|
|
411
|
+
return that;
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
hortis.isSelfUndetermined = function (name) {
|
|
415
|
+
return !name || name.includes("undetermined") || name.includes("various") || name.includes("Various") || name.includes("filamentous");
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
// Side-effects: populates that.obsIdToTaxon, that.undetHash, that.undetKeys, that.discardedTaxa
|
|
419
|
+
hortis.applyObservations = async function (that, obsRows, applySwaps) {
|
|
420
|
+
const identifiedTo = {}; // Two-level hash of {rank, obs id} to obs
|
|
421
|
+
const obsIdToRank = {}; // One-level hash of {obs id} to rank
|
|
422
|
+
for (let i = 0; i < obsRows.length; ++i) {
|
|
423
|
+
if (i % 100 === 0) {
|
|
424
|
+
console.log("Resolving taxon for observation " + i + "/" + obsRows.length);
|
|
425
|
+
}
|
|
426
|
+
const obs = obsRows[i];
|
|
427
|
+
const obsId = obs.observationId;
|
|
428
|
+
if (obsId === undefined) {
|
|
429
|
+
fluid.fail("Observation ", obs, " was not assigned an id");
|
|
430
|
+
}
|
|
431
|
+
const san = hortis.sanitizeSpeciesName(obs.taxonName);
|
|
432
|
+
let taxonLevel = "Undetermined";
|
|
433
|
+
if (!hortis.isSelfUndetermined(san)) {
|
|
434
|
+
const invertedId = applySwaps ? invertedSwaps[san] : null;
|
|
435
|
+
const taxon = invertedId && await that.source.get({id: invertedId}) || await that.source.get({name: san});
|
|
436
|
+
if (taxon && taxon.doc) {
|
|
437
|
+
taxonLevel = taxon.doc.rank;
|
|
438
|
+
const existing = that.obsIdToTaxon[obsId];
|
|
439
|
+
if (existing) {
|
|
440
|
+
fluid.fail("Error in dataset configuration - observation id " + obsId + " has previously been used to resolve taxon ", existing);
|
|
441
|
+
}
|
|
442
|
+
that.obsIdToTaxon[obsId] = taxon;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
fluid.set(identifiedTo, [taxonLevel, obsId], obs);
|
|
446
|
+
obsIdToRank[obsId] = taxonLevel;
|
|
447
|
+
}
|
|
448
|
+
fluid.each(identifiedTo, function (recs, taxonLevel) {
|
|
449
|
+
console.log("Identified " + Object.keys(recs).length + " records to " + taxonLevel);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
const undets = identifiedTo["Undetermined"];
|
|
453
|
+
|
|
454
|
+
if (that.options.discardRanksBelowIndex !== -1) {
|
|
455
|
+
hortis.ranks.forEach(function (rank, rankIndex) {
|
|
456
|
+
if (rankIndex < that.options.discardRanksBelowIndex) {
|
|
457
|
+
const toDiscard = identifiedTo[rank];
|
|
458
|
+
if (toDiscard && Object.keys(toDiscard).length) {
|
|
459
|
+
fluid.each(toDiscard, function (obs, obsId) {
|
|
460
|
+
const taxon = that.obsIdToTaxon[obsId];
|
|
461
|
+
fluid.set(that.discardedTaxa, [taxon.doc.name, obsId], obs);
|
|
462
|
+
delete that.obsIdToTaxon[obsId];
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
fluid.each(that.discardedTaxa, function (obsMap, taxonName) {
|
|
469
|
+
const keys = Object.keys(obsMap);
|
|
470
|
+
const firstObs = obsMap[keys[0]];
|
|
471
|
+
console.log("Discarded " + keys.length + " observations for taxon " + taxonName + " which were only identified to rank " + obsIdToRank[firstObs.observationId] + ":");
|
|
472
|
+
console.log(keys.join(", "));
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
fluid.each(undets, function (undet) {
|
|
476
|
+
if (!hortis.isSelfUndetermined(undet.taxonName)) {
|
|
477
|
+
that.undetHash[undet.taxonName] = undet;
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
const undetKeys = Object.keys(that.undetHash).sort();
|
|
481
|
+
console.log("\nListing " + undetKeys.length + " undetermined species in observations: \n");
|
|
482
|
+
undetKeys.forEach(function (undetKey) {
|
|
483
|
+
const row = that.undetHash[undetKey];
|
|
484
|
+
console.log(undetKey + (row.observationId ? (": " + row.observationId) : ""));
|
|
485
|
+
});
|
|
486
|
+
console.log();
|
|
487
|
+
that.undetKeys = undetKeys;
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
hortis.resolveObservationTaxa = async function (that, observations, outMap) {
|
|
491
|
+
const togo = [];
|
|
492
|
+
const taxa = Object.keys(outMap.columns);
|
|
493
|
+
for (let i = 0; i < observations.length; ++i) {
|
|
494
|
+
if (i % 100 === 0) {
|
|
495
|
+
console.log("Resolving ranks for observation " + i + "/" + observations.length);
|
|
496
|
+
}
|
|
497
|
+
const row = observations[i];
|
|
498
|
+
const taxon = that.obsIdToTaxon[row.observationId];
|
|
499
|
+
if (taxon) {
|
|
500
|
+
const outrow = fluid.copy(row);
|
|
501
|
+
outrow.iNaturalistTaxonName = taxon.doc.name;
|
|
502
|
+
outrow.iNaturalistTaxonId = taxon.doc.id;
|
|
503
|
+
// hortis.resolveTaxa(outrow, that.taxaById, taxon.taxonId, outMap.columns);
|
|
504
|
+
await hortis.iNat.getRanks(taxon.doc.id, outrow, that.source, taxa);
|
|
505
|
+
togo.push(outrow);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return togo;
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
hortis.applyPatches = function (resolved, patches) {
|
|
513
|
+
fluid.each(patches, function (patch, key) {
|
|
514
|
+
fluid.invokeGlobalFunction(patch.processor, [resolved, patch, key]);
|
|
515
|
+
});
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
// Summarise really means "Input was obs"
|
|
519
|
+
hortis.resolveAndFilter = async function (that, observations, filters, outMap, summarise) {
|
|
520
|
+
const resolved = {};
|
|
521
|
+
const outrows = await hortis.resolveObservationTaxa(that, observations, outMap);
|
|
522
|
+
|
|
523
|
+
resolved.filters = filters || {};
|
|
524
|
+
resolved.filterCount = Object.keys(resolved.filters).length;
|
|
525
|
+
|
|
526
|
+
const filtered = hortis.applyFilters(outrows, resolved.filters, resolved.filterCount);
|
|
527
|
+
|
|
528
|
+
if (summarise) {
|
|
529
|
+
resolved.obsRows = filtered;
|
|
530
|
+
} else {
|
|
531
|
+
resolved.summarisedRows = filtered;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return resolved;
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
hortis.writeReintegratedObservations = function (resolved, fileName, outMapFileName) {
|
|
538
|
+
// If we've been asked to summarise, also output obs since we must have input obs. Summarise means "input was obs"
|
|
539
|
+
if (resolved.obsRows) {
|
|
540
|
+
const reintegratedObsFile = hortis.obsifyFilename(fileName);
|
|
541
|
+
hortis.writeCSV(reintegratedObsFile, resolved.combinedObsOutMap.columns, resolved.obsRows, fluid.promise());
|
|
542
|
+
|
|
543
|
+
const combinedObsOutMapFilename = hortis.obsifyFilename(outMapFileName);
|
|
544
|
+
hortis.writeJSONSync(combinedObsOutMapFilename, resolved.combinedObsOutMap);
|
|
545
|
+
}
|
|
546
|
+
hortis.writeCSV(fileName, resolved.combinedOutMap.columns, resolved.summarisedRows, fluid.promise());
|
|
547
|
+
hortis.writeJSONSync(outMapFileName, resolved.combinedOutMap);
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
hortis.writeResolutionFile = function (that) {
|
|
551
|
+
const resolutionRows = fluid.transform(that.undetKeys, function (undetKey) {
|
|
552
|
+
return {
|
|
553
|
+
taxonName: undetKey,
|
|
554
|
+
commonName: that.undetHash[undetKey].commonName
|
|
555
|
+
};
|
|
556
|
+
});
|
|
557
|
+
const promise = fluid.promise();
|
|
558
|
+
hortis.writeCSV("taxonResolution.csv", taxonResolveMap.columns, resolutionRows, promise);
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
hortis.obsifyFilename = function (filename) {
|
|
562
|
+
const lastDot = filename.lastIndexOf(".");
|
|
563
|
+
return filename.substring(0, lastDot) + "-obs" + filename.substring(lastDot);
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const dataPromises = hortis.fusionToLoadable(fusion);
|
|
567
|
+
|
|
568
|
+
hortis.settleStructure(dataPromises).then(async function (data) {
|
|
569
|
+
await iNatTaxonSource.events.onCreate;
|
|
570
|
+
const summarise = parsedArgs.summarise || fusion.summarise;
|
|
571
|
+
const datasets = data.datasets;
|
|
572
|
+
let flatObs = [];
|
|
573
|
+
fluid.each(datasets, function (dataset) {
|
|
574
|
+
flatObs = flatObs.concat(dataset.obsRows);
|
|
575
|
+
});
|
|
576
|
+
console.log("Loaded " + flatObs.length + " observations from " + hortis.pluralise(Object.keys(datasets).length, "dataset"));
|
|
577
|
+
const that = hortis.makeTaxonomiser(iNatTaxonSource, {
|
|
578
|
+
discardRanksBelowIndex: discardRanksBelowIndex
|
|
579
|
+
});
|
|
580
|
+
await hortis.applyObservations(that, flatObs, fusion.applySwaps);
|
|
581
|
+
|
|
582
|
+
const writeResolutionFile = parsedArgs.writeRes; // TODO: doesn't seem that this is read or modified any more
|
|
583
|
+
if (writeResolutionFile) {
|
|
584
|
+
hortis.writeResolutionFile(that);
|
|
585
|
+
}
|
|
586
|
+
const outMaps = Object.values(fluid.getMembers(datasets, "outMap"));
|
|
587
|
+
const combinedOutMap = summarise ?
|
|
588
|
+
hortis.combineMaps([hortis.summariseCommonOutMap].concat({
|
|
589
|
+
counts: fusion.counts
|
|
590
|
+
}), true) :
|
|
591
|
+
hortis.combineMaps([hortis.commonOutMap].concat(outMaps), true);
|
|
592
|
+
|
|
593
|
+
combinedOutMap.datasets = fusion.datasets;
|
|
594
|
+
|
|
595
|
+
const resolved = await hortis.resolveAndFilter(that, flatObs, fusion.filters, combinedOutMap, summarise);
|
|
596
|
+
|
|
597
|
+
resolved.combinedOutMap = combinedOutMap;
|
|
598
|
+
resolved.combinedObsOutMap = hortis.combineMaps([hortis.commonObsOutMap].concat(outMaps), true);
|
|
599
|
+
|
|
600
|
+
if (summarise) {
|
|
601
|
+
hortis.applyPatches(resolved, data.patches);
|
|
602
|
+
resolved.summarisedRows = hortis.doSummarise(resolved.obsRows, true);
|
|
603
|
+
}
|
|
604
|
+
hortis.applyPatches(resolved, data.summaryPatches);
|
|
605
|
+
|
|
606
|
+
const reintegratedFilename = parsedArgs.dry ? "reintegrated.csv" : fluid.module.resolvePath(fusion.output);
|
|
607
|
+
|
|
608
|
+
hortis.writeReintegratedObservations(resolved, reintegratedFilename, fusion.combinedOutMap);
|
|
609
|
+
|
|
610
|
+
}, function (err) {
|
|
611
|
+
fluid.fail("Error loading data", err);
|
|
612
|
+
if (err.stack) {
|
|
613
|
+
console.log(err.stack);
|
|
614
|
+
}
|
|
615
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* eslint-env node */
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const fluid = require("infusion");
|
|
6
|
+
const minimist = require("minimist");
|
|
7
|
+
fluid.require("%imerss-bioinfo");
|
|
8
|
+
|
|
9
|
+
require("./dataProcessing/readCSV.js");
|
|
10
|
+
require("./dataProcessing/readCSVwithoutMap.js");
|
|
11
|
+
require("./dataProcessing/writeCSV.js");
|
|
12
|
+
|
|
13
|
+
const hortis = fluid.registerNamespace("hortis");
|
|
14
|
+
|
|
15
|
+
fluid.setLogging(true);
|
|
16
|
+
|
|
17
|
+
// Unscrew GBIF output by extracting back out into a stable field "scientificName" whatever is the most derived taxon name
|
|
18
|
+
// held in any taxon field
|
|
19
|
+
|
|
20
|
+
const parsedArgs = minimist(process.argv.slice(2));
|
|
21
|
+
|
|
22
|
+
const outputFile = parsedArgs.o || "unscrewed.csv";
|
|
23
|
+
const inputFile = parsedArgs._[0] || "%imerss-bioinfo/data/RBCM/RBCM_GBIF_records_intersected_with_Galiano_polygon_2021_03_08.tsv";
|
|
24
|
+
|
|
25
|
+
const input = hortis.csvReaderWithoutMap({
|
|
26
|
+
inputFile: fluid.module.resolvePath(inputFile),
|
|
27
|
+
csvOptions: {
|
|
28
|
+
// separator: "\t" // TODO: remember to put this back for real GBIF
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
input.completionPromise.then(function () {
|
|
32
|
+
const rows = input.rows.map(function (row) {
|
|
33
|
+
row.taxonName = row.species || row.genus || row.family || row.order || row["class"] || row.phylum || row.kingdom;
|
|
34
|
+
row.infraTaxonName = "";
|
|
35
|
+
const infra = row["infraspecificEpithet"];
|
|
36
|
+
if (infra) {
|
|
37
|
+
const rank = row.taxonRank === "VARIETY" ? " var. " : " ssp. ";
|
|
38
|
+
row.infraTaxonName += row.taxonName + rank + infra;
|
|
39
|
+
}
|
|
40
|
+
return row;
|
|
41
|
+
});
|
|
42
|
+
const completion = fluid.promise();
|
|
43
|
+
hortis.writeCSV(fluid.module.resolvePath(outputFile), Object.keys(rows[0]), rows, completion);
|
|
44
|
+
});
|