From 4e25f002905e0201374445be6f3f015868a7e173 Mon Sep 17 00:00:00 2001 From: Devin Jean Date: Wed, 6 Mar 2024 13:20:17 -0600 Subject: [PATCH] get parent/children of nodes --- .../global-biodiversity.js | 146 ++++++++++++++---- 1 file changed, 115 insertions(+), 31 deletions(-) diff --git a/src/procedures/global-biodiversity/global-biodiversity.js b/src/procedures/global-biodiversity/global-biodiversity.js index e172cca9..fa708be0 100644 --- a/src/procedures/global-biodiversity/global-biodiversity.js +++ b/src/procedures/global-biodiversity/global-biodiversity.js @@ -8,6 +8,7 @@ "use strict"; const ApiConsumer = require("../utils/api-consumer"); +const utils = require('../utils'); const types = require('../../input-types'); const _ = require('lodash'); @@ -29,6 +30,38 @@ const GBIF = new ApiConsumer( { cache: { ttl: 24 * 60 * 60 } }, ); +function cleanSpecies(r) { + const res = { + id: r['key'], + }; + + if (r['canonicalName'] !== undefined) { + res['canonicalName'] = r['canonicalName']; + } + if (r['vernacularNames'] !== undefined) { + res['vernacularNames'] = _.uniq(r['vernacularNames'].filter((x) => !x.language || x.language === 'eng').map((x) => x.vernacularName.toLowerCase())).sort() + } + if (r['extinct'] !== undefined) { + res['extinct'] = r['extinct']; + } + if (r['threatStatuses'] !== undefined) { + res['threatStatuses'] = _.uniq(r['threatStatuses'].map((x) => x.toLowerCase().replace('_', ' '))).sort(); + } + if (r['habitats'] !== undefined) { + res['habitats'] = _.uniq(r['habitats'].map((x) => x.toLowerCase())).sort(); + } + if (r['descriptions'] !== undefined) { + res['descriptions'] = r['descriptions'].filter((x) => !x.language || x.language === 'eng').map((x) => x.description); + } + for (const k of ['kingdom', 'phylum', 'class', 'order', 'family', 'genus', 'species']) { + if (r[k] !== undefined) { + res[k] = r[k]; + } + } + + return res; +} + /** * Search the database for species of the given common or scientific name. * @@ -55,52 +88,95 @@ GBIF.searchSpecies = async function (nameType, name, page = 1) { queryString: `datasetKey=${BackBone}&status=ACCEPTED&rank=SPECIES&qField=${nameType}&q=${encodeURIComponent(name)}&limit=${limit}&offset=${offset}`, }); - return res.results.map((r) => { - return { - id: r['key'], + return res.results.map((r) => cleanSpecies(r)); +}; - scientificName: r['canonicalName'], - commonNames: _.uniq(r['vernacularNames'].filter((x) => !x.language || x.language === 'eng').map((x) => x.vernacularName.toLowerCase())).sort(), +/** + * Get the taxonomical parent of a node/entry in the tree of life. + * + * @param {BoundedInteger<0>} id id of the taxonomy entry to get the parent of + * @returns {Object} taxonomical parent of the entry + */ +GBIF.getParent = async function (id) { + let res; + try { + res = await this._requestData({ + path: `species/${id}/parents`, + }); + } catch (e) { + throw Error(`unknown taxonomy node: ${id}`); + } - extinct: r['extinct'], - threatStatuses: _.uniq(r['threatStatuses'].map((x) => x.toLowerCase().replace('_', ' '))).sort(), + if (res.length === 0) { + throw Error(`taxonomy node ${id} has no parents`); + } - habitats: _.uniq(r['habitats'].map((x) => x.toLowerCase())).sort(), - descriptions: r['descriptions'].filter((x) => !x.language || x.language === 'eng').map((x) => x.description), + return cleanSpecies(res[res.length - 1]); +}; - kingdom: r['kingdom'], - phylum: r['phylum'], - class: r['class'], - order: r['order'], - family: r['family'], - genus: r['genus'], - species: r['species'], - }; - }); +/** + * Get the taxonomical children of a node/entry in the tree of life. + * + * Because a node may have many children, only up to 20 children are returned per call to this RPC. + * You can check if there are more children by increasing the ``page`` number of children to return. + * When there are no more children, an empty list is returned. + * + * @param {BoundedInteger<0>} id id of the taxonomy entry to get the children of + * @param {BoundedInteger<1>=} page page number of results to return (default ``1``) + * @returns {Array} taxonomical children of the entry + */ +GBIF.getChildren = async function (id, page = 1) { + const limit = 20; + const offset = (page - 1) * limit; + + // their api puts a hard cap at offset 100000 - just give empty results + if (offset > 100000) { + return []; + } + + let res; + try { + res = await this._requestData({ + path: `species/${id}/children`, + queryString: `limit=${limit}&offset=${offset}`, + }); + } catch (e) { + throw Error(`unknown taxonomy node: ${id}`); + } + + return res.results.map((r) => cleanSpecies(r)); }; /** - * Get the URL of any image(s) associated with a particular species returned by :func:`GlobalBiodiversity.searchSpecies`. - * These URLs can then be passed to :func:`GlobalBiodiversity.getImage` to retrieve the image. + * Get the URL of any media associated with a particular taxonomy node/entry in the tree of life. + * These URLs can then be passed to :func:`GlobalBiodiversity.getImage` or :func:`GlobalBiodiversity.getSound` to get the image/sound. * - * Because there may be many associated images, only up to 20 URLs are returned per call to this RPC. - * You can check if there are more images by increasing the ``page`` number of results to return. - * When there are no more images, an empty list is returned. + * Because there may be many associated media entries, only up to 20 URLs are returned per call to this RPC. + * You can check if there are more URLs by increasing the ``page`` number of results to return. + * When there are no more URLs, an empty list is returned. * - * @param {BoundedInteger<0>} speciesId the id of a species returned by :func:`GlobalBiodiversity.searchSpecies` + * @param {Enum=} type the type of media to return + * @param {BoundedInteger<0>} id id of the taxonomy node to get media from * @param {BoundedInteger<1>=} page page number of results to return (default ``1``) - * @returns {Array} zero or more associated image URLs + * @returns {Array} zero or more associated media URLs */ -GBIF.getImageURLs = async function (speciesId, page = 1) { +GBIF.getMediaURLs = async function (type, id, page = 1) { + type = { 'image': 'StillImage', 'sound': 'Sound' }[type]; + const limit = 20; const offset = (page - 1) * limit; - const res = await this._requestData({ - path: `species/${speciesId}/media`, - queryString: `limit=${limit}&offset=${offset}`, - }); + let res; + try { + res = await this._requestData({ + path: `species/${id}/media`, + queryString: `limit=${limit}&offset=${offset}`, + }); + } catch (e) { + throw Error(`unknown taxonomy node: ${id}`); + } - return res.results.map((r) => r.identifier); + return res.results.filter((r) => r.type === type).map((r) => r.identifier); }; /** @@ -113,4 +189,12 @@ GBIF.getImage = async function (url) { return this._sendImage({ baseUrl: url }); }; +/** + * Get a sound from a sound URL returned by :func:`GlobalBiodiversity.getMediaURLs`. + */ +GBIF.getSound = async function (url) { + const data = await this._requestData({ baseUrl: url }); + return utils.sendAudioBuffer(this.response, data); +} + module.exports = GBIF;