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,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
|
+
};
|