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,150 @@
1
+
2
+ /* eslint-env node */
3
+
4
+ "use strict";
5
+
6
+ const fluid = require("infusion");
7
+ const hortis = fluid.registerNamespace("hortis");
8
+
9
+ fluid.defaults("hortis.iNat.obsSource", {
10
+ gradeNames: "kettle.dataSource.URL",
11
+ url: "https://api.inaturalist.org/v1/observations",
12
+ port: 443,
13
+ invokers: {
14
+ resolveUrl: {
15
+ funcName: "hortis.resolveUrl", // url, termMap, directModel as sent from fluid.dataSource.URL.handle
16
+ args: ["{arguments}.0", "{that}.options.paramMap", "{arguments}.2"]
17
+ }
18
+ },
19
+ paramMap: {
20
+ // place_id: 94935,
21
+ // project_id: 5799,
22
+ // taxon_id: 1,
23
+ quality_grade: "research",
24
+ order: "asc",
25
+ order_by: "id",
26
+ id_above: "%id_above",
27
+ per_page: "%per_page"
28
+ }
29
+ });
30
+
31
+ hortis.paramsToSearch = function (params) {
32
+ const encoded = fluid.hashToArray(fluid.transform(params, function (value, key) {
33
+ return encodeURIComponent(key) + "=" + encodeURIComponent(value);
34
+ }));
35
+ return encoded.join("&");
36
+ };
37
+
38
+ hortis.resolveUrl = function (url, paramMap, directModel) {
39
+ const params = fluid.transform(paramMap, function (value) {
40
+ const path = typeof(value) === "string" && value.startsWith("%") ? value.substring(1) : null;
41
+ const paramValue = path ? fluid.get(directModel, path) : value;
42
+ // TODO: Encode arrays as CSV here
43
+ return fluid.isValue(paramValue) ? paramValue : fluid.NO_VALUE;
44
+ });
45
+ const urlObj = new fluid.resourceLoader.UrlClass(url);
46
+
47
+ urlObj.search = hortis.paramsToSearch(params);
48
+ return urlObj.toString();
49
+ };
50
+
51
+ hortis.iNat.obsMap = {
52
+ "id": "id",
53
+ "time_observed_at": "time_observed_at",
54
+ "observed_on": "observed_on",
55
+ "user_login": "user.login",
56
+ "user_name": "user.name",
57
+ "quality_grade": "quality_grade",
58
+ "coordinates_obscured": "obscured",
59
+ "image_url": "observation_photos.0.photo.url", // replace "small" with "medium"
60
+ // "latitude": "location.0",
61
+ // "longitude": "location.1",
62
+ "positional_accuracy": "positional_accuracy",
63
+ // "private_latitude": "private_location[0]",
64
+ // "private_longitude": "private_location[1]",
65
+ "scientific_name": "taxon.name",
66
+ "common_name": "taxon.preferred_common_name",
67
+ "taxon_id": "taxon.id"
68
+ };
69
+
70
+ hortis.applyLocation = function (oneResult, row, prefix) {
71
+ const key = prefix + "location";
72
+ const location = typeof(oneResult[key]) === "string" ? oneResult[key].split(",") : [];
73
+ row[prefix + "latitude"] = location[0];
74
+ row[prefix + "longitude"] = location[1];
75
+ };
76
+
77
+ hortis.resultToRow = function (oneResult) {
78
+ const row = fluid.transform(hortis.iNat.obsMap, function (value) {
79
+ return fluid.get(oneResult, value);
80
+ });
81
+ hortis.applyLocation(oneResult, row, "");
82
+ hortis.applyLocation(oneResult, row, "private_");
83
+ row.time_observed_at = row.time_observed_at || row.observed_on;
84
+ return row;
85
+ };
86
+
87
+ hortis.pushResultRows = function (rows, response) {
88
+ response.results.forEach(function (oneResult) {
89
+ const row = hortis.resultToRow(oneResult);
90
+ rows.push(row);
91
+ });
92
+ };
93
+
94
+ // var obsURL = "https://api.inaturalist.org/v1/observations?place_id=94935&taxon_id=1&quality_grade=research&order=asc&order_by=id";
95
+
96
+
97
+ fluid.defaults("hortis.iNatObsById", {
98
+ gradeNames: ["kettle.dataSource.URL", "hortis.withINatRateLimit"],
99
+ url: "https://api.inaturalist.org/v1/observations/%id",
100
+ termMap: {
101
+ id: "%id"
102
+ }
103
+ });
104
+
105
+ fluid.defaults("hortis.cachediNatObsById", {
106
+ gradeNames: "hortis.cachedApiSource",
107
+ components: {
108
+ apiSource: {
109
+ type: "hortis.iNatObsById"
110
+ },
111
+ dbSource: {
112
+ type: "hortis.iNatObsAPI.byIdDBSource"
113
+ }
114
+ },
115
+ invokers: {
116
+ upgradeLiveDocument: "hortis.cachediNatObsById.upgradeLiveDocument"
117
+ }
118
+ });
119
+
120
+ hortis.cachediNatObsById.upgradeLiveDocument = function (query, live) {
121
+ return {
122
+ fetched_at: new Date().toISOString(),
123
+ id: query.id,
124
+ doc: live
125
+ };
126
+ };
127
+
128
+
129
+ fluid.defaults("hortis.iNatObsAPI.byIdDBSource", {
130
+ gradeNames: ["hortis.listableSqliteSource"],
131
+ createString: "CREATE TABLE IF NOT EXISTS iNatObsId (id INTEGER PRIMARY KEY, fetched_at TEXT, doc BLOB)",
132
+ columnCodecs: {
133
+ doc: "zlib"
134
+ },
135
+ readQuery: {
136
+ query: "SELECT id, fetched_at, doc from iNatObsId WHERE id = ?",
137
+ args: ["%id"]
138
+ },
139
+ writeQuery: {
140
+ query: "INSERT OR REPLACE INTO iNatObsId (id, fetched_at, doc) VALUES ($id, $fetched_at, $doc)",
141
+ args: {
142
+ $id: "%id",
143
+ $fetched_at: "%fetched_at",
144
+ $doc: "%doc"
145
+ }
146
+ },
147
+ listQuery: {
148
+ query: "SELECT id from iNatObsId"
149
+ }
150
+ });
@@ -0,0 +1,460 @@
1
+ /* eslint-env node */
2
+
3
+ "use strict";
4
+
5
+ const fluid = require("infusion");
6
+ const hortis = fluid.registerNamespace("hortis");
7
+ const moment = require("moment");
8
+
9
+ fluid.registerNamespace("hortis.iNat");
10
+
11
+ require("../utils/dataSource.js");
12
+ require("../utils/utils.js");
13
+ require("../dataProcessing/sqlite.js");
14
+ require("./obsAPI.js");
15
+ require("kettle"); // for kettle.dataSource.URL
16
+
17
+ hortis.ranks = fluid.freezeRecursive(fluid.require("%imerss-bioinfo/data/ranks.json"));
18
+
19
+ hortis.sanitizeSpeciesName = function (name) {
20
+ name = name.trim();
21
+ // Examples such as Blidingia spp., including Blidingia minima var. vexata
22
+ [" sp.", " spp.", "?", " / ", " x "].forEach(function (toRemove) {
23
+ const index = name.indexOf(toRemove);
24
+ // Special exception to allow us to process Myxicola sp.A and sp.B, as well as Haliclona sp.1 and sp.2 etc.
25
+ if (index !== -1 && !name.match(/sp\.[A-Z0-9]/)) {
26
+ name = name.substring(0, index);
27
+ }
28
+ });
29
+ name = name.replace("�", "ue");
30
+ name = name.replace(/ (\(.*\))/g, "");
31
+ name = name.replace(" ssp.", "");
32
+ name = name.replace(" subsp.", "");
33
+ name = name.replace(" grp.", "");
34
+ name = name.replace(" grp", "");
35
+ name = name.replace(" group", "");
36
+ name = name.replace(" var.", "");
37
+ name = name.replace(" ined.", "");
38
+ name = name.replace(" etc.", "");
39
+ name = name.replace(" aff.", "");
40
+ name = name.replace(" agg.", "");
41
+ name = name.replace(" s.lat.", "");
42
+ name = name.replace(" s. lat.", "");
43
+ name = name.replace(" f.", "");
44
+ name = name.replace(" species complex", "");
45
+ name = name.replace(" complex", "");
46
+ name = name.replace(" cf ", " ");
47
+ name = name.replace(" ?", " ");
48
+ name = name.replace(" x ", " × ");
49
+ return name;
50
+ };
51
+
52
+ hortis.iNat.filenameFromTaxonId = function (taxonAPIFileBase, id) {
53
+ return taxonAPIFileBase + "/" + id + ".json";
54
+ };
55
+
56
+ hortis.iNat.loadTaxonDoc = function (taxonAPIFileBase, id) {
57
+ const filename = hortis.iNat.filenameFromTaxonId(taxonAPIFileBase, id);
58
+ return hortis.readJSONSync(filename);
59
+ };
60
+
61
+ hortis.iNat.parentTaxaIds = function (taxonDoc) {
62
+ return taxonDoc.ancestry ? taxonDoc.ancestry.split("/").reverse() : [];
63
+ };
64
+
65
+ fluid.defaults("hortis.iNatAPILimiter", {
66
+ gradeNames: ["fluid.dataSource.rateLimiter", "fluid.resolveRootSingle"],
67
+ rateLimit: 1300,
68
+ singleRootType: "hortis.iNatAPILimiter"
69
+ });
70
+
71
+ // Create a singleton rate limiter to ensure that all requests against the real iNaturalist API are rate-limited
72
+ // regardless of source
73
+ hortis.iNatAPILimiter();
74
+
75
+ // A mixin for any dataSource which will rate limit its requests against the singleton iNaturalist rate limiter
76
+ fluid.defaults("hortis.withINatRateLimit", {
77
+ listeners: {
78
+ "onRead.rateLimitBefore": {
79
+ priority: "before:impl",
80
+ func: "{hortis.iNatAPILimiter}.requestStart"
81
+ },
82
+ "onRead.rateLimitAfter": {
83
+ priority: "after:impl",
84
+ func: "{hortis.iNatAPILimiter}.requestEnd"
85
+ }
86
+ }
87
+ });
88
+
89
+ // Note that the taxon API at https://www.inaturalist.org/taxa/877359.json , e.g gives different results to the
90
+ // "modern" one at https://api.inaturalist.org/v1/taxa/877359
91
+ // The former will return alternate names in the "taxon_names" block and the latter does not. But the former can
92
+ // only accept exact ids.
93
+
94
+ fluid.defaults("hortis.iNatTaxonById", {
95
+ gradeNames: ["kettle.dataSource.URL", "hortis.withINatRateLimit"],
96
+ url: "https://www.inaturalist.org/taxa/%id.json",
97
+ termMap: {
98
+ id: "%id"
99
+ }
100
+ });
101
+
102
+ fluid.defaults("hortis.iNatTaxonByName", {
103
+ gradeNames: ["kettle.dataSource.URL", "hortis.withINatRateLimit"],
104
+ url: "https://api.inaturalist.org/v1/taxa?q=%name",
105
+ termMap: {
106
+ name: "%name"
107
+ }
108
+ });
109
+
110
+
111
+ hortis.DAY_IN_MS = 24 * 60 * 60 * 1000;
112
+
113
+ fluid.defaults("hortis.cachedApiSource", {
114
+ gradeNames: ["fluid.dataSource", "fluid.dataSource.noencoding"],
115
+ refreshInDays: 31,
116
+ disableCache: false,
117
+ components: {
118
+ inMemorySource: {
119
+ type: "fluid.inMemoryCachedSource"
120
+ }
121
+ //apiSource,
122
+ //dbSource
123
+ },
124
+ invokers: {
125
+ // accepts "payload", "live" in order to convert API document to DB form
126
+ upgradeLiveDocument: "fluid.notImplemented"
127
+ },
128
+ listeners: {
129
+ "onRead.impl": {
130
+ func: "hortis.cachedApiSource.read",
131
+ args: ["{that}", "{arguments}.0", "{arguments}.1"]
132
+ }
133
+ }
134
+ });
135
+
136
+ hortis.readNoValue = function (value) {
137
+ return value === fluid.NO_VALUE ? undefined : value;
138
+ };
139
+
140
+ hortis.writeNoValue = function (value) {
141
+ return value === undefined ? fluid.NO_VALUE : value;
142
+ };
143
+
144
+ hortis.cachedApiSource.read = async function (that, payload, options) {
145
+ const query = options.directModel;
146
+ const firstLevel = await that.inMemorySource.get(query);
147
+ if (firstLevel) {
148
+ return hortis.readNoValue(firstLevel);
149
+ } else {
150
+ const cached = await that.dbSource.get(query);
151
+ if (cached && !that.options.disableCache) {
152
+ const staleness = Date.now() - Date.parse(cached.fetched_at);
153
+ if (staleness < that.options.refreshInDays * hortis.DAY_IN_MS) {
154
+ await that.inMemorySource.set(hortis.writeNoValue(query), cached);
155
+ return cached;
156
+ } else {
157
+ console.log("Staleness is " + moment.duration(staleness).humanize() + " so fetching live document");
158
+ }
159
+ }
160
+ const live = await that.apiSource.get(query);
161
+ const toWrite = await that.upgradeLiveDocument(query, live);
162
+ if (live) { // Only write to db if we got live
163
+ await that.dbSource.set(query, toWrite);
164
+ }
165
+ // Always write to top-level cache
166
+ await that.inMemorySource.set(hortis.writeNoValue(query), toWrite);
167
+ return toWrite;
168
+ }
169
+ };
170
+
171
+ fluid.defaults("hortis.cachediNatTaxonById", {
172
+ gradeNames: "hortis.cachedApiSource",
173
+ components: {
174
+ apiSource: {
175
+ type: "hortis.iNatTaxonById"
176
+ },
177
+ dbSource: {
178
+ type: "hortis.iNatTaxonAPI.byIdDBSource"
179
+ }
180
+ },
181
+ invokers: {
182
+ upgradeLiveDocument: "hortis.cachediNatTaxonById.upgradeLiveDocument"
183
+ }
184
+ });
185
+
186
+ hortis.cachediNatTaxonById.upgradeLiveDocument = function (query, live) {
187
+ return {
188
+ fetched_at: new Date().toISOString(),
189
+ id: query.id,
190
+ doc: live
191
+ };
192
+ };
193
+
194
+ fluid.defaults("hortis.cachediNatTaxonByName", {
195
+ gradeNames: "hortis.cachedApiSource",
196
+ components: {
197
+ apiSource: {
198
+ type: "hortis.iNatTaxonByName"
199
+ },
200
+ dbSource: {
201
+ type: "hortis.iNatTaxonAPI.byNameDBSource"
202
+ }
203
+ // byIdSource
204
+ },
205
+ invokers: {
206
+ upgradeLiveDocument: "hortis.cachediNatTaxonByName.upgradeLiveDocument({that}.byIdSource, {arguments}.0, {arguments}.1)"
207
+ }
208
+ });
209
+
210
+ hortis.bestNameMatch = function (results, query) {
211
+ const match = results.findIndex(function (result) {
212
+ return result.matched_term === query.name || result.name === query.name;
213
+ });
214
+ return results[match === -1 ? 0 : match];
215
+ };
216
+
217
+ hortis.cachediNatTaxonByName.upgradeLiveDocument = async function (byIdSource, query, live) {
218
+ console.log("upgradeLive byName given ", live);
219
+ const togo = {
220
+ name: query.name,
221
+ fetched_at: new Date().toISOString()
222
+ };
223
+ if (live && live.results.length > 0) {
224
+ const record = hortis.bestNameMatch(live.results, query);
225
+ const doc = fluid.filterKeys(record, ["name", "rank", "id"]);
226
+
227
+ const byIdBack = await byIdSource.get({id: doc.id});
228
+ const nameRecord = byIdBack.doc.taxon_names.find(function (nameRec) {
229
+ return nameRec.name === query.name;
230
+ });
231
+ let nameStatus;
232
+ if (nameRecord) {
233
+ fluid.extend(doc, fluid.filterKeys(nameRecord, ["is_valid", "lexicon", "created_at", "updated_at"]));
234
+ nameStatus = nameRecord.is_valid ? "accepted" : "unaccepted";
235
+ } else {
236
+ nameStatus = "invalid";
237
+ }
238
+ doc.nameStatus = nameStatus;
239
+ console.log("Filtered byName document to ", doc);
240
+ togo.doc = doc;
241
+ console.log("About to write ", togo);
242
+ return togo;
243
+ }
244
+ return togo;
245
+ };
246
+
247
+ hortis.iNat.jwtDistribution = function (jwt) {
248
+ return jwt ? "hortis.iNat.distributeJWT" : [];
249
+ };
250
+
251
+ hortis.iNat.computeHeaders = function (jwt) {
252
+ return {
253
+ Authorization: "Bearer " + jwt.api_token
254
+ };
255
+ };
256
+
257
+ fluid.defaults("hortis.iNat.distributeJWT", {
258
+ jwtHeaders: "@expand:hortis.iNat.computeHeaders({that}.options.jwt)",
259
+ distributeOptions: {
260
+ distributeJWT: {
261
+ target: "{that kettle.dataSource.URL}.options.headers",
262
+ source: "{that}.options.jwtHeaders"
263
+ }
264
+ }
265
+ });
266
+
267
+ // The top-level overall cached iNaturalist taxon and obs source
268
+ fluid.defaults("hortis.iNatTaxonSource", {
269
+ gradeNames: ["fluid.dataSource", "fluid.dataSource.noencoding", "{that}.jwtDistribution"],
270
+ disableCache: false,
271
+ distributeOptions: {
272
+ disableCache: {
273
+ source: "{that}.options.disableCache",
274
+ target: "{that hortis.cachedApiSource}.options.disableCache"
275
+ }
276
+ },
277
+ invokers: {
278
+ jwtDistribution: "hortis.iNat.jwtDistribution({that}.options.jwt)"
279
+ },
280
+ components: {
281
+ db: {
282
+ type: "hortis.sqliteDB",
283
+ options: {
284
+ dbFile: "%imerss-bioinfo/data/iNaturalist/taxa.db/taxa.sqlite3"
285
+ }
286
+ },
287
+ byId: {
288
+ type: "hortis.cachediNatTaxonById"
289
+ },
290
+ byName: {
291
+ type: "hortis.cachediNatTaxonByName",
292
+ options: {
293
+ components: {
294
+ byIdSource: "{hortis.cachediNatTaxonById}"
295
+ }
296
+ }
297
+ },
298
+ obsById: {
299
+ type: "hortis.cachediNatObsById"
300
+ }
301
+ },
302
+ listeners: {
303
+ "onRead.impl": {
304
+ func: "hortis.iNatTaxonSource.read",
305
+ args: ["{that}", "{arguments}.0", "{arguments}.1"]
306
+ }
307
+ }
308
+ });
309
+
310
+ /** Accepts queries either by name or by id, forwarding to the two raw API sources which are attached.
311
+ * In the case of a by id query, it is sent direct.
312
+ * In the case of a by name query, we look at the first result in the list and attempt to look it up by id and then
313
+ * look at the "taxon_names" entries which account for iNat's alternate names. If we find it there, we can decide
314
+ * that the entry is either "accepted" (if iNat says is_valid), "unaccepted" (if not is_valid)
315
+ * or "invalid" if it is not there, because iNat has returned the result as a partial or fuzzy name match.
316
+ * @param {Component} that - The iNatTaxonSource component
317
+ * @param {Object} payload - The payload being transformed
318
+ * @param {Object} options - The DataSource chain options, including the query in "directModel"
319
+ * @return {Promise<Object>} A promise for the document as returned from the API
320
+ */
321
+ hortis.iNatTaxonSource.read = async function (that, payload, options) {
322
+ const query = options.directModel;
323
+ let doc;
324
+ if (query.id) {
325
+ doc = await that.byId.get(query);
326
+ } else if (query.name) {
327
+ doc = await that.byName.get(query);
328
+ } else if (query.obsId) {
329
+ doc = await that.obsById.get({id: query.obsId});
330
+ }
331
+ return doc;
332
+ };
333
+
334
+ fluid.defaults("fluid.dataSource.listable", {
335
+ invokers: {
336
+ list: "fluid.notImplemented"
337
+ }
338
+ });
339
+
340
+ fluid.defaults("hortis.iNatTaxonAPI.byIdDBSource", {
341
+ gradeNames: ["hortis.listableSqliteSource"],
342
+ createString: "CREATE TABLE IF NOT EXISTS iNatTaxaId (id INTEGER PRIMARY KEY, fetched_at TEXT, doc BLOB)",
343
+ columnCodecs: {
344
+ doc: "zlib"
345
+ },
346
+ readQuery: {
347
+ query: "SELECT id, fetched_at, doc from iNatTaxaId WHERE id = ?",
348
+ args: ["%id"]
349
+ },
350
+ writeQuery: {
351
+ query: "INSERT OR REPLACE INTO iNatTaxaId (id, fetched_at, doc) VALUES ($id, $fetched_at, $doc)",
352
+ args: {
353
+ $id: "%id",
354
+ $fetched_at: "%fetched_at",
355
+ $doc: "%doc"
356
+ }
357
+ },
358
+ listQuery: {
359
+ query: "SELECT id from iNatTaxaId"
360
+ }
361
+ });
362
+
363
+ fluid.defaults("hortis.iNatTaxonAPI.byNameDBSource", {
364
+ gradeNames: ["hortis.listableSqliteSource"],
365
+ createString: "CREATE TABLE IF NOT EXISTS iNatTaxaName (name TEXT PRIMARY KEY, fetched_at TEXT, doc BLOB)",
366
+ columnCodecs: {
367
+ doc: "zlib"
368
+ },
369
+ readQuery: {
370
+ query: "SELECT name, fetched_at, doc from iNatTaxaName WHERE name = ?",
371
+ args: ["%name"]
372
+ },
373
+ writeQuery: {
374
+ query: "INSERT OR REPLACE INTO iNatTaxaName (name, fetched_at, doc) VALUES ($name, $fetched_at, $doc)",
375
+ args: {
376
+ $name: "%name",
377
+ $fetched_at: "%fetched_at",
378
+ $doc: "%doc"
379
+ }
380
+ },
381
+ listQuery: {
382
+ query: "SELECT name from iNatTaxaName"
383
+ }
384
+ });
385
+
386
+ fluid.defaults("hortis.iNatTaxonAPI.dbTaxonAPIs", {
387
+ gradeNames: "fluid.modelComponent",
388
+ components: {
389
+ db: {
390
+ type: "hortis.sqliteDB",
391
+ options: {
392
+ dbFile: "%imerss-bioinfo/data/iNaturalist/taxa.db/taxa.sqlite3"
393
+ }
394
+ },
395
+ byId: {
396
+ type: "hortis.iNatTaxonAPI.byIdDBSource"
397
+ },
398
+ byName: {
399
+ type: "hortis.iNatTaxonAPI.byNameDBSource"
400
+ }
401
+ }
402
+ });
403
+
404
+ hortis.extractSpeciesName = function (name) {
405
+ const words = name.split(" ");
406
+ return words[1];
407
+ };
408
+
409
+ // Plan in the end is for multitaxonomy source which federates ids as "id:iNat:", names as "name:iNat:"
410
+
411
+ /** Return ancestry documents starting from the specified taxon and continuing up the hierarchy to the root. Note that a
412
+ * rank of "complex" will be considered to be "species".
413
+ * @param {String} id - The id of the taxon for which ancestry is required
414
+ * @param {fluid.dataSource} byIdSource - The dataSource serving taxon documents by ID
415
+ * @return {Promise<Array<Object>>} - A promise for an array of taxon documents, with the taxon document for which the
416
+ * id was specified in the first element
417
+ */
418
+ hortis.iNat.getAncestry = async function (id, byIdSource) {
419
+ const baseDoc = await byIdSource.get({id: id});
420
+ const parentIds = hortis.iNat.parentTaxaIds(baseDoc.doc);
421
+ // TODO: Should not really be parallel
422
+ const allDocs = await Promise.all(parentIds.map(async id => byIdSource.get({id: id})));
423
+ return [baseDoc, ...allDocs];
424
+ };
425
+
426
+ /**
427
+ * Resolve the higher taxon ranks of a taxon into a supplied structure, given a taxon Id
428
+ * @param {String} id - The id of the taxon whose ranks are to be fetched
429
+ * @param {Object} rankTarget - The structure to receive the ranks. If `fields` is not set, any existing rank fields
430
+ * in this structure will be overwritten - otherwise just the fields listed in `fields` will be overwritten
431
+ * @param {fluid.dataSource} byIdSource - The taxon source
432
+ * @param {Array<String>} [fields] - [optional] If supplied, only the taxa whose keys are listed in this structure will be
433
+ * fetched into `rankTarget` - these are uncapitalized
434
+ * @return {Promise<Object>} The structure `rankTarget` with higher taxa filled in
435
+ */
436
+ hortis.iNat.getRanks = async function (id, rankTarget, byIdSource, fields) {
437
+ const allDocs = await hortis.iNat.getAncestry(id, byIdSource);
438
+ const indexed = {};
439
+ allDocs.forEach(function (doc) {
440
+ const docRank = doc.doc.rank;
441
+ const rank = hortis.capitalize(docRank === "complex" ? "species" : docRank);
442
+ indexed[rank] = rank === "Species" ? hortis.extractSpeciesName(doc.doc.name) : doc.doc.name;
443
+ });
444
+ // console.log("Got ranks ", Object.keys(indexed));
445
+ hortis.ranks.forEach(function (lowRank) {
446
+ const rank = hortis.capitalize(lowRank);
447
+ let targetRank = fields ? lowRank : rank;
448
+ if (lowRank in rankTarget) {
449
+ targetRank = lowRank;
450
+ }
451
+ if (fields && fields.includes(targetRank) || targetRank in rankTarget) {
452
+ const thisRank = indexed[rank];
453
+ const oldTarget = rankTarget[targetRank];
454
+ if (thisRank && oldTarget && oldTarget !== thisRank) {
455
+ console.log("Replacing " + targetRank + " " + oldTarget + " with " + thisRank + " for taxon " + rankTarget.taxonName);
456
+ }
457
+ rankTarget[targetRank] = thisRank || "";
458
+ }
459
+ });
460
+ };