521 lines
14 KiB
JavaScript
521 lines
14 KiB
JavaScript
/*!
|
|
* 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,
|
|
headers: { "Api-User-Agent": USER_AGENT },
|
|
dataType: "jsonp",
|
|
});
|
|
}
|
|
|
|
/**
|
|
*/
|
|
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;
|
|
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,
|
|
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").click();
|
|
return;
|
|
}
|
|
if (e && e.which === $.ui.keyCode.ENTER && query.length >= 2) {
|
|
$("#btnSearch").click();
|
|
return;
|
|
}
|
|
if (query === lastQuery || query.length < 2) {
|
|
console.log("Ignored query '" + query + "'");
|
|
return;
|
|
}
|
|
$(this).data("lastQuery", query);
|
|
_delay("search", 1, function() {
|
|
$("#btnSearch").click();
|
|
});
|
|
$("#btnResetSearch").attr("disabled", query.length === 0);
|
|
$("#btnSearch").attr("disabled", query.length < 2);
|
|
})
|
|
.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);
|