demo/taxonomy-browser/taxonomy-browser.js
/*!
* Fancytree Taxonomy Browser
*
* Copyright (c) 2015, Martin Wendt (https://wwWendt.de)
*
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version @VERSION
* @date @DATE
*/
/* global Handlebars */
/* eslint-disable no-console */
(function ($, window, document) {
"use strict";
/*******************************************************************************
* Private functions and variables
*/
var taxonTree,
searchResultTree,
tmplDetails,
tmplInfoPane,
tmplMedia,
timerMap = {},
// USER_AGENT = "Fancytree Taxonomy Browser/1.0",
GBIF_URL = "//api.gbif.org/v1/",
TAXONOMY_KEY = "d7dddbf4-2cf0-4f39-9b2a-bb099caae36c", // GBIF backbone taxonomy
SEARCH_PAGE_SIZE = 5,
CHILD_NODE_PAGE_SIZE = 200,
glyphOpts = {
preset: "bootstrap3",
map: {
expanderClosed: "glyphicon glyphicon-menu-right", // glyphicon-plus-sign
expanderLazy: "glyphicon glyphicon-menu-right", // glyphicon-plus-sign
expanderOpen: "glyphicon glyphicon-menu-down", // glyphicon-collapse-down
},
};
// Load and compile handlebar templates
$.get("details.tmpl.html", function (data) {
tmplDetails = Handlebars.compile(data);
Handlebars.registerPartial("tmplDetails", tmplDetails);
});
$.get("media.tmpl.html", function (data) {
tmplMedia = Handlebars.compile(data);
Handlebars.registerPartial("tmplMedia", tmplMedia);
});
$.get("info-pane.tmpl.html", function (data) {
tmplInfoPane = Handlebars.compile(data);
});
/** Update UI elements according to current status
*/
function updateControls() {
var query = $.trim($("input[name=query]").val());
$("#btnPin").attr("disabled", !taxonTree.getActiveNode());
$("#btnUnpin")
.attr("disabled", !taxonTree.isFilterActive())
.toggleClass("btn-success", taxonTree.isFilterActive());
$("#btnResetSearch").attr("disabled", query.length === 0);
$("#btnSearch").attr("disabled", query.length < 2);
}
/**
* Invoke callback after `ms` milliseconds.
* Any pending action of this type is cancelled before.
*/
function _delay(tag, ms, callback) {
/*jshint -W040:true */
var self = this;
tag = "" + (tag || "default");
if (timerMap[tag] != null) {
clearTimeout(timerMap[tag]);
delete timerMap[tag];
// console.log("Cancel timer '" + tag + "'");
}
if (ms == null || callback == null) {
return;
}
// console.log("Start timer '" + tag + "'");
timerMap[tag] = setTimeout(function () {
// console.log("Execute timer '" + tag + "'");
callback.call(self);
}, +ms);
}
/**
*/
function _callWebservice(cmd, data) {
return $.ajax({
url: GBIF_URL + cmd,
data: $.extend({}, data),
cache: true,
// 2022-11-10: Datatype 'JSONP' no longer works:
// '[Error] Refused to execute http://api.gbif.org/v1/species/... as script because "X-Content-Type-Options: nosniff" was given and its Content-Type is not a script MIME type.
// We rely on CORS, but this only works if no additoinal header is set
// headers: { "Api-User-Agent": USER_AGENT },
dataType: "json",
});
}
/**
*/
function updateItemDetails(key) {
$("#tmplDetails").addClass("busy");
$.bbq.pushState({ key: key });
$.when(
_callWebservice("species/" + key),
_callWebservice("species/" + key + "/speciesProfiles"),
_callWebservice("species/" + key + "/synonyms"),
_callWebservice("species/" + key + "/descriptions"),
_callWebservice("species/" + key + "/media")
).done(function (species, profiles, synonyms, descriptions, media) {
// Requests are resolved as: [ data, statusText, jqXHR ]
species = species[0];
profiles = profiles[0];
synonyms = synonyms[0];
descriptions = descriptions[0];
media = media[0];
var info = $.extend(species, {
profileList: profiles.results, // marine, extinct
profile:
profiles.results.length === 1 ? profiles.results[0] : null, // marine, extinct
synonyms: synonyms.results,
descriptions: descriptions.results,
descriptionsByLang: {},
media: media.results,
now: new Date().toString(),
});
$.each(info.descriptions, function (i, o) {
if (!info.descriptionsByLang[o.language]) {
info.descriptionsByLang[o.language] = [];
}
info.descriptionsByLang[o.language].push(o);
});
console.log("updateItemDetails", info);
$("#tmplDetails")
// .html(tmplDetails(info))
.removeClass("busy");
$("#tmplMedia")
// .html(tmplMedia(info))
.removeClass("busy");
$("#tmplInfoPane").html(tmplInfoPane(info)).removeClass("busy");
$("[data-toggle='popover']").popover();
$(".carousel").carousel();
$("#mediaCounter").text("" + (media.results.length || ""));
// $("[data-toggle='collapse']").collapse();
updateControls();
});
}
/**
*/
function updateBreadcrumb(key, loadTreeNodes) {
var $ol = $("ol.breadcrumb").addClass("busy"),
activeNode = taxonTree.getActiveNode();
if (activeNode && activeNode.key !== key) {
activeNode.setActive(false); // deactivate, in case the new key is not found
}
$.when(
_callWebservice("species/" + key + "/parents"),
_callWebservice("species/" + key)
).done(function (parents, node) {
// Both requests resolved (result format: [ data, statusText, jqXHR ])
var nodeList = parents[0],
keyList = [];
nodeList.push(node[0]);
// Display as <OL> list (for Bootstrap breadcrumbs)
$ol.empty().removeClass("busy");
$.each(nodeList, function (i, o) {
var name =
o.vernacularName || o.canonicalName || o.scientificName;
keyList.push(o.key);
if ("" + o.key === "" + key) {
$ol.append(
$("<li class='active'>").append(
$("<span>", {
text: name,
title: o.rank,
})
)
);
} else {
$ol.append(
$("<li>").append(
$("<a>", {
href: "#key=" + o.key,
text: name,
title: o.rank,
})
)
);
}
});
if (loadTreeNodes) {
// console.log("updateBreadcrumb - loadKeyPath", keyList);
taxonTree.loadKeyPath(
"/" + keyList.join("/"),
function (n, status) {
// console.log("... updateBreadcrumb - loadKeyPath " + n.title + ": " + status);
switch (status) {
case "loaded":
n.makeVisible();
break;
case "ok":
n.setActive();
// n.makeVisible();
break;
}
}
);
}
});
}
/**
*/
function search(query) {
query = $.trim(query);
console.log("searching for '" + query + "'...");
// Store the source options for optional paging
searchResultTree.lastSourceOpts = {
// url: GBIF_URL + "species/match", // Fuzzy matches scientific names against the GBIF Backbone Taxonomy
url: GBIF_URL + "species/search", // Full text search of name usages covering the scientific and vernacular name, the species description, distribution and the entire classification across all name usages of all or some checklists
data: {
q: query,
datasetKey: TAXONOMY_KEY,
// name: query,
// strict: "true",
// hl: true,
limit: SEARCH_PAGE_SIZE,
offset: 0,
},
cache: true,
// headers: { "Api-User-Agent": USER_AGENT }
// dataType: "jsonp"
};
$("#searchResultTree").addClass("busy");
searchResultTree
.reload(searchResultTree.lastSourceOpts)
.done(function (result) {
// console.log("search returned", result);
if (result.length < 1) {
searchResultTree.getRootNode().setStatus("nodata");
}
$("#searchResultTree").removeClass("busy");
// https://github.com/tbasse/jquery-truncate
// SLOW!
// $("div.truncate").truncate({
// multiline: true
// });
updateControls();
});
}
/*******************************************************************************
* Pageload Handler
*/
$(function () {
$("#taxonTree").fancytree({
extensions: ["filter", "glyph", "wide"],
filter: {
mode: "hide",
},
glyph: glyphOpts,
autoCollapse: true,
activeVisible: true,
autoScroll: true,
source: {
url: GBIF_URL + "species/root/" + TAXONOMY_KEY,
data: {},
cache: true,
// dataType: "jsonp"
},
init: function (event, data) {
updateControls();
$(window).trigger("hashchange"); // trigger on initial page load
},
lazyLoad: function (event, data) {
data.result = {
url: GBIF_URL + "species/" + data.node.key + "/children",
data: {
limit: CHILD_NODE_PAGE_SIZE,
},
cache: true,
// dataType: "jsonp"
};
// store this request options for later paging
data.node.lastSourceOpts = data.result;
},
postProcess: function (event, data) {
var response = data.response;
data.node.info("taxonTree postProcess", response);
data.result = $.map(response.results, function (o) {
return (
o && {
title:
o.vernacularName ||
o.canonicalName ||
o.scientificName,
key: o.key,
nubKey: o.nubKey,
folder: true,
lazy: true,
}
);
});
if (response.endOfRecords === false) {
// Allow paging
data.result.push({
title: "(more)",
statusNodeType: "paging",
});
} else {
// No need to store the extra data
delete data.node.lastSourceOpts;
}
},
activate: function (event, data) {
$("#tmplDetails").addClass("busy");
$("ol.breadcrumb").addClass("busy");
updateControls();
_delay("showDetails", 500, function () {
updateItemDetails(data.node.key);
updateBreadcrumb(data.node.key);
});
},
clickPaging: function (event, data) {
// Load the next page of results
var source = $.extend(
true,
{},
data.node.parent.lastSourceOpts
);
source.data.offset = data.node.parent.countChildren() - 1;
data.node.replaceWith(source);
},
});
$("#searchResultTree").fancytree({
extensions: ["table", "wide"],
source: [{ title: "No Results." }],
minExpandLevel: 2,
icon: false,
table: {
nodeColumnIdx: 2,
},
postProcess: function (event, data) {
var response = data.response;
data.node.info("search postProcess", response);
data.result = $.map(response.results, function (o) {
var res = $.extend(
{
title: o.scientificName,
key: o.key,
},
o
);
return res;
});
// Append paging link
if (
response.count != null &&
response.offset + response.limit < response.count
) {
data.result.push({
title:
"(" +
(response.count -
response.offset -
response.limit) +
" more)",
statusNodeType: "paging",
});
}
data.node.info("search postProcess 2", data.result);
},
// loadChildren: function(event, data) {
// $("#searchResultTree td div.cell").truncate({
// multiline: true
// });
// },
renderColumns: function (event, data) {
var i,
node = data.node,
$tdList = $(node.tr).find(">td"),
cnList = node.data.vernacularNames
? $.map(node.data.vernacularNames, function (o) {
return o.vernacularName;
})
: [];
i = 0;
function _setCell($cell, text) {
$("<div class='truncate'>")
.attr("title", text)
.text(text)
.appendTo($cell);
}
$tdList.eq(i++).text(node.key);
$tdList.eq(i++).text(node.data.rank);
i++; // #1: node.title = scientificName
// $tdList.eq(i++).text(cnList.join(", "));
_setCell($tdList.eq(i++), cnList.join(", "));
$tdList.eq(i++).text(node.data.canonicalName);
// $tdList.eq(i++).text(node.data.accordingTo);
_setCell($tdList.eq(i++), node.data.accordingTo);
$tdList.eq(i++).text(node.data.taxonomicStatus);
$tdList.eq(i++).text(node.data.nameType);
$tdList.eq(i++).text(node.data.numOccurrences);
$tdList.eq(i++).text(node.data.numDescendants);
// $tdList.eq(i++).text(node.data.authorship);
_setCell($tdList.eq(i++), node.data.authorship);
// $tdList.eq(i++).text(node.data.publishedIn);
_setCell($tdList.eq(i++), node.data.publishedIn);
},
activate: function (event, data) {
if (data.node.isStatusNode()) {
return;
}
_delay("activateNode", 500, function () {
updateItemDetails(data.node.key);
updateBreadcrumb(data.node.key);
});
},
clickPaging: function (event, data) {
// Load the next page of results
var source = $.extend(
true,
{},
searchResultTree.lastSourceOpts
);
source.data.offset = data.node.parent.countChildren() - 1;
data.node.replaceWith(source);
},
});
taxonTree = $.ui.fancytree.getTree("#taxonTree");
searchResultTree = $.ui.fancytree.getTree("#searchResultTree");
// Bind a callback that executes when document.location.hash changes.
// (This code uses bbq: https://github.com/cowboy/jquery-bbq)
$(window).on("hashchange", function (e) {
var key = $.bbq.getState("key");
console.log("bbq key", key);
if (key) {
updateBreadcrumb(key, true);
}
}); // don't trigger now, since we need the the taxonTree root nodes to be loaded first
$("input[name=query]")
.on("keyup", function (e) {
var query = $.trim($(this).val()),
lastQuery = $(this).data("lastQuery");
if ((e && e.which === $.ui.keyCode.ESCAPE) || query === "") {
$("#btnResetSearch").trigger("click");
return;
}
if (e && e.which === $.ui.keyCode.ENTER && query.length >= 2) {
$("#btnSearch").trigger("click");
return;
}
if (query === lastQuery || query.length < 2) {
console.log("Ignored query '" + query + "'");
return;
}
$(this).data("lastQuery", query);
_delay("search", 1, function () {
$("#btnSearch").trigger("click");
});
$("#btnResetSearch").attr("disabled", query.length === 0);
$("#btnSearch").attr("disabled", query.length < 2);
})
.trigger("focus");
$("#btnResetSearch").click(function (e) {
$("#searchResultPane").collapse("hide");
$("input[name=query]").val("");
searchResultTree.clear();
updateControls();
});
$("#btnSearch")
.click(function (event) {
$("#searchResultPane").collapse("show");
search($("input[name=query]").val());
})
.attr("disabled", true);
$("#btnPin").click(function (event) {
taxonTree.filterBranches(function (n) {
return n.isActive();
});
updateControls();
});
$("#btnUnpin").click(function (event) {
taxonTree.clearFilter();
updateControls();
});
// -----------------------------------------------------------------------------
}); // end of pageload handler
})(jQuery, window, document);