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.
Files changed (182) hide show
  1. package/README.md +156 -0
  2. package/build.js +196 -0
  3. package/docs/css/imerss-viz-all.css +1227 -0
  4. package/docs/css/imerss-viz-wp-overrides.css +70 -0
  5. package/docs/css/pepiowelh.css +76 -0
  6. package/docs/css/xetthecum-external.css +47 -0
  7. package/docs/css/xetthecum-shared.css +80 -0
  8. package/docs/css/xetthecum.css +526 -0
  9. package/docs/js/imerss-viz-all.js +58 -0
  10. package/docs/js/imerss-viz-all.js.map +1 -0
  11. package/docs/js/jquery.js +10881 -0
  12. package/index.html +81 -0
  13. package/index.js +7 -0
  14. package/indexWithJustMap.html +92 -0
  15. package/indexWithMap.html +79 -0
  16. package/indexWithMapHoweSound.html +91 -0
  17. package/indexWithMapPepiowelh.html +119 -0
  18. package/indexWithMapValdes.html +90 -0
  19. package/indexWithMapXetthecum.html +90 -0
  20. package/package.json +50 -0
  21. package/src/WoRMS/taxonAPI.js +13 -0
  22. package/src/arphify.js +662 -0
  23. package/src/assignNames.js +103 -0
  24. package/src/auxBuild/restoreJQuery.js +1 -0
  25. package/src/buildSource/Comprehensive Lists.html +42 -0
  26. package/src/buildSource/Data Paper Part I Visualisation.html +50 -0
  27. package/src/buildSource/Galiano Life List.html +43 -0
  28. package/src/buildSource/Metchosin/index.html +22 -0
  29. package/src/buildSource/Pe'pi'ow'elh.html +72 -0
  30. package/src/buildSource/Squamish Life List.html +48 -0
  31. package/src/buildSource/Valdes Island Biodiversity.html +64 -0
  32. package/src/buildSource/Xetthecum.html +40 -0
  33. package/src/buildSource/dataPaperSunburstAndMap.html +30 -0
  34. package/src/buildSource/index.html +224 -0
  35. package/src/client/css/imerss-viz-wp-overrides.css +70 -0
  36. package/src/client/css/imerss-viz.css +531 -0
  37. package/src/client/css/pepiowelh.css +76 -0
  38. package/src/client/css/xetthecum-external.css +47 -0
  39. package/src/client/css/xetthecum-shared.css +80 -0
  40. package/src/client/css/xetthecum.css +526 -0
  41. package/src/client/html/imerss-viz-map-only.html +75 -0
  42. package/src/client/html/imerss-viz-map-pepi.html +77 -0
  43. package/src/client/html/imerss-viz-map-xetthecum.html +90 -0
  44. package/src/client/html/imerss-viz-map.html +75 -0
  45. package/src/client/html/imerss-viz.html +58 -0
  46. package/src/client/html/importing Xetthecum patterns.txt +10 -0
  47. package/src/client/html/xetthecum-patterns.html +27 -0
  48. package/src/client/img/Animalia_2.svg +1 -0
  49. package/src/client/img/Animalia_Annelida.svg +655 -0
  50. package/src/client/img/Animalia_Chaetognatha.svg +72 -0
  51. package/src/client/img/Animalia_Cnidaria.svg +173 -0
  52. package/src/client/img/Animalia_Ctenophora.svg +298 -0
  53. package/src/client/img/Animalia_Nemertea.svg +54 -0
  54. package/src/client/img/Animalia_Phoronida.svg +271 -0
  55. package/src/client/img/Animalia_Platyhelminthes.svg +58 -0
  56. package/src/client/img/Animalia_Porifera.svg +220 -0
  57. package/src/client/img/Animalia_Sipuncula.svg +155 -0
  58. package/src/client/img/Arachnoidiscus_schmidtii_web_1-2-scaled.webp +0 -0
  59. package/src/client/img/Arrow-down.svg +11 -0
  60. package/src/client/img/Arrow-up.svg +6 -0
  61. package/src/client/img/Chromista.svg +1 -0
  62. package/src/client/img/Pepiowelh/BlackNoWe2-wb-bb.png +0 -0
  63. package/src/client/img/Pepiowelh/BlackNoWe2-wb.png +0 -0
  64. package/src/client/img/Pepiowelh/BlackNoWe2.png +0 -0
  65. package/src/client/img/Pepiowelh/Ecocultural Mapping Banner.svg +1 -0
  66. package/src/client/img/Plankton.jpg +0 -0
  67. package/src/client/img/Plantae.svg +1 -0
  68. package/src/client/img/Plantae_2.svg +1 -0
  69. package/src/client/img/Protista.svg +1 -0
  70. package/src/client/img/Valdes/Valdes Island Conservancy.jpg +0 -0
  71. package/src/client/img/Xetthecum/Agricultural.jpg +0 -0
  72. package/src/client/img/Xetthecum/Anthropogenic_banner.jpg +0 -0
  73. package/src/client/img/Xetthecum/Broad_Leaf.jpg +0 -0
  74. package/src/client/img/Xetthecum/Caves.jpg +0 -0
  75. package/src/client/img/Xetthecum/Coastal_Bluffs.jpg +0 -0
  76. package/src/client/img/Xetthecum/Conifer_Pole_Sapling.jpg +0 -0
  77. package/src/client/img/Xetthecum/Developed.jpg +0 -0
  78. package/src/client/img/Xetthecum/Dock.jpg +0 -0
  79. package/src/client/img/Xetthecum/Eelgrass.jpg +0 -0
  80. package/src/client/img/Xetthecum/Forests_banner.jpg +0 -0
  81. package/src/client/img/Xetthecum/Freshwater_banner.jpg +0 -0
  82. package/src/client/img/Xetthecum/Greig_Creek.jpg +0 -0
  83. package/src/client/img/Xetthecum/Intertidal.jpg +0 -0
  84. package/src/client/img/Xetthecum/Laughlin_Lake.jpg +0 -0
  85. package/src/client/img/Xetthecum/Marine_banner.jpg +0 -0
  86. package/src/client/img/Xetthecum/Mature_Conifer_Forest.jpg +0 -0
  87. package/src/client/img/Xetthecum/Porlier_Pass_Road.jpg +0 -0
  88. package/src/client/img/Xetthecum/Riparian.jpg +0 -0
  89. package/src/client/img/Xetthecum/Rural.jpg +0 -0
  90. package/src/client/img/Xetthecum/Subtidal.jpg +0 -0
  91. package/src/client/img/Xetthecum/Wetland.jpg +0 -0
  92. package/src/client/img/Xetthecum/Woodland.jpg +0 -0
  93. package/src/client/img/Xetthecum/Woodland_banner.jpg +0 -0
  94. package/src/client/img/Xetthecum/Xetthecum.jpg +0 -0
  95. package/src/client/img/Xetthecum/Young_Conifer_Forest.jpg +0 -0
  96. package/src/client/img/Xetthecum/Young_Mixed_Forest.jpg +0 -0
  97. package/src/client/img/Xetthecum/food.svg +38 -0
  98. package/src/client/img/Xetthecum/indicator.svg +52 -0
  99. package/src/client/img/Xetthecum/material.svg +186 -0
  100. package/src/client/img/Xetthecum/medicinal.svg +62 -0
  101. package/src/client/img/Xetthecum/missing-value.svg +3 -0
  102. package/src/client/img/Xetthecum/spiritual.svg +13 -0
  103. package/src/client/img/Xetthecum/trade.svg +74 -0
  104. package/src/client/img/audio.svg +5 -0
  105. package/src/client/img/external-link.svg +3 -0
  106. package/src/client/img/selected-eye.svg +4 -0
  107. package/src/client/img/selected-tick.svg +3 -0
  108. package/src/client/img/thumbs/BCLeps.jpg +0 -0
  109. package/src/client/img/thumbs/Checklist.jpg +0 -0
  110. package/src/client/img/thumbs/Data-Paper-Figma.jpg +0 -0
  111. package/src/client/img/thumbs/Galiano-Data-Paper.jpg +0 -0
  112. package/src/client/img/thumbs/Galiano.jpg +0 -0
  113. package/src/client/img/thumbs/Maxwell.jpg +0 -0
  114. package/src/client/img/thumbs/Squamish.jpg +0 -0
  115. package/src/client/img/thumbs/Valdes.jpg +0 -0
  116. package/src/client/img/thumbs/Xetthecum.jpg +0 -0
  117. package/src/client/img/up-arrow.svg +4 -0
  118. package/src/client/js/autocomplete.js +53 -0
  119. package/src/client/js/checklist.js +179 -0
  120. package/src/client/js/colour.js +139 -0
  121. package/src/client/js/datasetControls.js +212 -0
  122. package/src/client/js/imerss-viz.js +1397 -0
  123. package/src/client/js/leafletMap.js +357 -0
  124. package/src/client/js/leafletMapWithGrid.js +337 -0
  125. package/src/client/js/leafletMapWithRegions.js +381 -0
  126. package/src/client/js/renderSVG.js +108 -0
  127. package/src/client/js/tabs.js +53 -0
  128. package/src/client/js/xetthecum.js +106 -0
  129. package/src/client/json/emptyPhyloMap.json +1 -0
  130. package/src/client/json/kingdomPhyloMap.json +17 -0
  131. package/src/client/json/phyloMap.json +58 -0
  132. package/src/compare.js +97 -0
  133. package/src/compilecsv.js +47 -0
  134. package/src/dataProcessing/LZ4.js +27 -0
  135. package/src/dataProcessing/coordinatePatch.js +151 -0
  136. package/src/dataProcessing/readCSV.js +107 -0
  137. package/src/dataProcessing/readCSVwithMap.js +90 -0
  138. package/src/dataProcessing/readCSVwithoutMap.js +25 -0
  139. package/src/dataProcessing/readJSON.js +23 -0
  140. package/src/dataProcessing/sqlite.js +174 -0
  141. package/src/dataProcessing/summarise.js +122 -0
  142. package/src/dataProcessing/taxonScrape.js +14 -0
  143. package/src/dataProcessing/worker.js +31 -0
  144. package/src/dataProcessing/writeCSV.js +34 -0
  145. package/src/dataProcessing/writeJSON.js +15 -0
  146. package/src/dbtaxa.js +96 -0
  147. package/src/deAuthorise.js +48 -0
  148. package/src/deqgis.js +148 -0
  149. package/src/expressions/expr.js +65 -0
  150. package/src/expressions/jsep.js +925 -0
  151. package/src/filterFirst.js +51 -0
  152. package/src/geom/geoJSON.js +121 -0
  153. package/src/iNaturalist/examples/api.inaturalist.org-v1-taxa!q=Acmispon americanus americanus.json +200 -0
  154. package/src/iNaturalist/examples/api.inaturalist.org-v1-taxa!q=Flabellina japonica.json +61 -0
  155. package/src/iNaturalist/examples/www.inaturalist.org-taxa-877359.json +132 -0
  156. package/src/iNaturalist/iNatUrls.js +17 -0
  157. package/src/iNaturalist/obsAPI.js +150 -0
  158. package/src/iNaturalist/taxonAPI.js +460 -0
  159. package/src/inatObs.js +120 -0
  160. package/src/inattify.js +123 -0
  161. package/src/intersect.js +80 -0
  162. package/src/intersectFilter.js +48 -0
  163. package/src/landlocked.js +137 -0
  164. package/src/lib/jquery.binarytransport.js +51 -0
  165. package/src/lib/lz4-commentary.txt +9 -0
  166. package/src/lib/lz4.js +73 -0
  167. package/src/lib/point-in-polygon.js +36 -0
  168. package/src/lz4.js +40 -0
  169. package/src/marmalise.js +333 -0
  170. package/src/materialise.js +68 -0
  171. package/src/reintegrate.js +71 -0
  172. package/src/taxonomise.js +615 -0
  173. package/src/unscrewGBIF.js +44 -0
  174. package/src/utils/dataSource.js +173 -0
  175. package/src/utils/settleStructure.js +62 -0
  176. package/src/utils/utils.js +92 -0
  177. package/src/viz/plethify.js +39 -0
  178. package/src/wormify.js +154 -0
  179. package/src/xetthecum/xetthecumFilters.js +126 -0
  180. package/test/testDataSource.js +182 -0
  181. package/test/testiNatObsSource.js +38 -0
  182. 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
+ });