mar10/fancytree

View on GitHub
demo/taxonomy-browser/taxonomy-browser.js

Summary

Maintainability
F
3 days
Test Coverage
/*!
 * 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);