define([
"jquery",
"underscore",
"backbone",
"collections/SolrResults",
"models/Search",
"models/MetricsModel",
"common/Utilities",
"views/SearchResultView",
"views/searchSelect/AnnotationFilterView",
"text!templates/search.html",
"text!templates/statCounts.html",
"text!templates/pager.html",
"text!templates/mainContent.html",
"text!templates/currentFilter.html",
"text!templates/loading.html",
"gmaps",
"nGeohash",
], function (
$,
_,
Backbone,
SearchResults,
SearchModel,
MetricsModel,
Utilities,
SearchResultView,
AnnotationFilter,
CatalogTemplate,
CountTemplate,
PagerTemplate,
MainContentTemplate,
CurrentFilterTemplate,
LoadingTemplate,
gmaps,
nGeohash,
) {
"use strict";
/**
* @class DataCatalogView
* @classcategory Views
* @extends Backbone.View
* @constructor
* @deprecated
* @description This view is deprecated and will eventually be removed in a future version (likely 3.0.0)
*/
var DataCatalogView = Backbone.View.extend(
/** @lends DataCatalogView.prototype */ {
el: "#Content",
isSubView: false,
filters: true, // Turn on/off the filters in this view
/**
* If true, the view height will be adjusted to fit the height of the window
* If false, the view height will be fixed via CSS
* @type {Boolean}
*/
fixedHeight: false,
// The default global models for searching
searchModel: null,
searchResults: null,
statsModel: null,
mapModel: null,
/**
* The templates for this view
* @type {Underscore.template}
*/
template: _.template(CatalogTemplate),
statsTemplate: _.template(CountTemplate),
pagerTemplate: _.template(PagerTemplate),
mainContentTemplate: _.template(MainContentTemplate),
currentFilterTemplate: _.template(CurrentFilterTemplate),
loadingTemplate: _.template(LoadingTemplate),
metricStatTemplate: _.template(
"<span class='metric-icon'> <i class='icon" +
" <%=metricIcon%>'></i> </span>" +
"<span class='metric-value'> <i class='icon metric-icon'>" +
"</i> </span>",
),
// Search mode
mode: "map",
// Map settings and storage
map: null,
ready: false,
allowSearch: true,
hasZoomed: false,
hasDragged: false,
markers: {},
tiles: [],
tileCounts: [],
/**
* The general error message to show as a title in the error box when there
* is an error fetching results from solr
* @type {string}
* @default "Something went wrong while getting the list of datasets"
* @since 2.15.0
*/
solrErrorTitle: "Something went wrong while getting the list of datasets",
/**
* The user-friendly text to show when a solr request gives a status 500
* error. If none is provided, then the error message that is returned from
* solr will be displayed.
* @type {string}
* @since 2.15.0
*/
solrError500Message: null,
// Contains the geohashes for all the markers on the map (if turned on in the Map model)
markerGeohashes: [],
// Contains all the info windows for all the markers on the map (if turned on in the Map model)
markerInfoWindows: [],
// Contains all the info windows for each document in the search result list - to display on hover
tileInfoWindows: [],
// Contains all the currently visible markers on the map
resultMarkers: [],
// The geohash value for each tile drawn on the map
tileGeohashes: [],
mapFilterToggle: ".toggle-map-filter",
// Delegated events for creating new items, and clearing completed ones.
events: {
"click #results_prev": "prevpage",
"click #results_next": "nextpage",
"click #results_prev_bottom": "prevpage",
"click #results_next_bottom": "nextpage",
"click .pagerLink": "navigateToPage",
"click .filter.btn": "updateTextFilters",
"keypress input[type='text'].filter": "triggerOnEnter",
"focus input[type='text'].filter": "getAutocompletes",
"change #sortOrder": "triggerSearch",
"change #min_year": "updateYearRange",
"change #max_year": "updateYearRange",
"click #publish_year": "updateYearRange",
"click #data_year": "updateYearRange",
"click .remove-filter": "removeFilter",
"click input[type='checkbox'].filter": "updateBooleanFilters",
"click #clear-all": "resetFilters",
"click .remove-addtl-criteria": "removeAdditionalCriteria",
"click .collapse-me": "collapse",
"click .filter-contain .expand-collapse-control":
"toggleFilterCollapse",
"click #toggle-map": "toggleMapMode",
"click .toggle-map": "toggleMapMode",
"click .toggle-list": "toggleList",
"click .toggle-map-filter": "toggleMapFilter",
"mouseover .open-marker": "showResultOnMap",
"mouseout .open-marker": "hideResultOnMap",
"mouseover .prevent-popover-runoff": "preventPopoverRunoff",
},
initialize: function (options) {
var view = this;
// Get all the options and apply them to this view
if (options) {
var optionKeys = Object.keys(options);
_.each(optionKeys, function (key, i) {
view[key] = options[key];
});
}
},
// Render the main view and/or re-render subviews. Don't call .html() here
// so we don't lose state, rather use .setElement(). Delegate rendering
// and event handling to sub views
render: function () {
// Use the global models if there are no other models specified at time of render
if (
MetacatUI.appModel.get("searchHistory").length > 0 &&
(!this.searchModel || Object.keys(this.searchModel).length == 0)
) {
var lastSearchModels = _.last(
MetacatUI.appModel.get("searchHistory"),
);
if (lastSearchModels) {
if (lastSearchModels.search) {
this.searchModel = lastSearchModels.search.clone();
}
if (lastSearchModels.map) {
this.mapModel = lastSearchModels.map.clone();
}
}
} else if (
typeof MetacatUI.appSearchModel !== "undefined" &&
(!this.searchModel || Object.keys(this.searchModel).length == 0)
) {
this.searchModel = MetacatUI.appSearchModel;
this.mapModel = MetacatUI.mapModel;
this.statsModel = MetacatUI.statsModel;
}
if (!this.mapModel && gmaps) {
this.mapModel = MetacatUI.mapModel;
}
if (
(typeof this.searchResults === "undefined" ||
!this.searchResults ||
Object.keys(this.searchResults).length == 0) &&
MetacatUI.appSearchResults &&
Object.keys(MetacatUI.appSearchResults).length > 0
) {
this.searchResults = MetacatUI.appSearchResults;
if (!this.statsModel) {
this.statsModel = MetacatUI.statsModel;
}
if (!this.mapModel) {
this.mapModel = MetacatUI.mapModel;
}
}
// Get the search mode - either "map" or "list"
if (typeof this.mode === "undefined" || !this.mode) {
this.mode = MetacatUI.appModel.get("searchMode");
if (typeof this.mode === "undefined" || !this.mode) {
this.mode = "map";
}
MetacatUI.appModel.set("searchMode", this.mode);
}
if ($(window).outerWidth() <= 600) {
this.mode = "list";
MetacatUI.appModel.set("searchMode", "list");
gmaps = null;
}
if (!this.isSubView) {
MetacatUI.appModel.set("headerType", "default");
$("body").addClass("DataCatalog");
} else {
this.$el.addClass("DataCatalog");
}
// Populate the search template with some model attributes
var loadingHTML = this.loadingTemplate({
msg: "Retrieving member nodes...",
});
var templateVars = {
gmaps: gmaps,
mode: MetacatUI.appModel.get("searchMode"),
useMapBounds: this.searchModel.get("useGeohash"),
username: MetacatUI.appUserModel.get("username"),
isMySearch:
_.indexOf(
this.searchModel.get("username"),
MetacatUI.appUserModel.get("username"),
) > -1,
loading: loadingHTML,
searchModelRef: this.searchModel,
searchResultsRef: this.searchResults,
dataSourceTitle:
MetacatUI.theme == "dataone" ? "Member Node" : "Data source",
};
var cel = this.template(
_.extend(this.searchModel.toJSON(), templateVars),
);
this.$el.html(cel);
//Hide the filters that are disabled in the AppModel settings
_.each(
this.$(".filter-contain[data-category]"),
function (filterEl) {
if (
!_.contains(
MetacatUI.appModel.get("defaultSearchFilters"),
$(filterEl).attr("data-category"),
)
) {
$(filterEl).hide();
}
},
this,
);
// Store some references to key views that we use repeatedly
this.$resultsview = this.$("#results-view");
this.$results = this.$("#results");
// Update stats
this.updateStats();
// Render the Google Map
this.renderMap();
// Initialize the tooltips
var tooltips = $(".tooltip-this");
// Find the tooltips that are on filter labels - add a slight delay to those
var groupedTooltips = _.groupBy(tooltips, function (t) {
return (
($(t).prop("tagName") == "LABEL" ||
$(t).parent().prop("tagName") == "LABEL") &&
$(t).parents(".filter-container").length > 0
);
});
var forFilterLabel = true,
forOtherElements = false;
$(groupedTooltips[forFilterLabel]).tooltip({
delay: {
show: "800",
},
});
$(groupedTooltips[forOtherElements]).tooltip();
// Initialize all popover elements
$(".popover-this").popover();
// Initialize the resizeable content div
$("#content").resizable({
handles: "n,s,e,w",
});
// Collapse the filters
this.toggleFilterCollapse();
// Iterate through each search model text attribute and show UI filter for each
var categories = [
"all",
"attribute",
"creator",
"id",
"taxon",
"spatial",
"additionalCriteria",
"annotation",
"isPrivate",
];
var thisTerm = null;
for (var i = 0; i < categories.length; i++) {
thisTerm = this.searchModel.get(categories[i]);
if (thisTerm === undefined || thisTerm === null) break;
for (var x = 0; x < thisTerm.length; x++) {
this.showFilter(categories[i], thisTerm[x]);
}
}
// List the Member Node filters
var view = this;
_.each(
_.contains(
MetacatUI.appModel.get("defaultSearchFilters"),
"dataSource",
),
function (source, i) {
view.showFilter("dataSource", source);
},
);
// Add the custom query under the "Anything" filter
if (this.searchModel.get("customQuery")) {
this.showFilter("all", this.searchModel.get("customQuery"));
}
// Register listeners; this is done here in render because the HTML
// needs to be bound before the listenTo call can be made
this.stopListening(this.searchResults);
this.stopListening(this.searchModel);
this.stopListening(MetacatUI.appModel);
this.listenTo(this.searchResults, "reset", this.cacheSearch);
this.listenTo(this.searchResults, "add", this.addOne);
this.listenTo(this.searchResults, "reset", this.addAll);
this.listenTo(this.searchResults, "reset", this.checkForProv);
this.listenTo(this.searchResults, "error", this.showError);
// List data sources
this.listDataSources();
this.listenTo(
MetacatUI.nodeModel,
"change:members",
this.listDataSources,
);
// listen to the MetacatUI.appModel for the search trigger
this.listenTo(MetacatUI.appModel, "search", this.getResults);
this.listenTo(
MetacatUI.appUserModel,
"change:loggedIn",
this.triggerSearch,
);
// and go to a certain page if we have it
this.getResults();
// Set a custom height on any elements that have the .auto-height class
if ($(".auto-height").length > 0 && !this.fixedHeight) {
// Readjust the height whenever the window is resized
$(window).resize(this.setAutoHeight);
$(".auto-height-member").resize(this.setAutoHeight);
}
this.addAnnotationFilter();
return this;
},
/**
* addAnnotationFilter - Add the annotation filter to the view
*/
addAnnotationFilter: function () {
if (MetacatUI.appModel.get("bioportalAPIKey")) {
var view = this;
var popoverTriggerSelector =
"[data-category='annotation'] .expand-collapse-control";
if (!this.$el.find(popoverTriggerSelector)) {
return;
}
var annotationFilter = new AnnotationFilter({
popoverTriggerSelector: popoverTriggerSelector,
});
this.$el.find(popoverTriggerSelector).append(annotationFilter.el);
annotationFilter.render();
annotationFilter.off("annotationSelected");
annotationFilter.on("annotationSelected", function (event, item) {
$("#annotation_input").val(item.value);
view.updateTextFilters(event, item);
});
}
},
// Linked Data Object for appending the jsonld into the browser DOM
getLinkedData: function () {
// Find the MN info from the CN Node list
var members = MetacatUI.nodeModel.get("members");
for (var i = 0; i < members.length; i++) {
if (
members[i].identifier ==
MetacatUI.nodeModel.get("currentMemberNode")
) {
var nodeModelObject = members[i];
}
}
// JSON Linked Data Object
let elJSON = {
"@context": {
"@vocab": "http://schema.org/",
},
"@type": "DataCatalog",
};
if (nodeModelObject) {
// "keywords": "",
// "provider": "",
let conditionalData = {
description: nodeModelObject.description,
identifier: nodeModelObject.identifier,
image: nodeModelObject.logo,
name: nodeModelObject.name,
url: nodeModelObject.url,
};
$.extend(elJSON, conditionalData);
}
// Check if the jsonld already exists from the previous data view
// If not create a new script tag and append otherwise replace the text for the script
if (!document.getElementById("jsonld")) {
var el = document.createElement("script");
el.type = "application/ld+json";
el.id = "jsonld";
el.text = JSON.stringify(elJSON);
document.querySelector("head").appendChild(el);
} else {
var script = document.getElementById("jsonld");
script.text = JSON.stringify(elJSON);
}
return;
},
/*
* Sets the height on elements in the main content area to fill up the entire area minus header and footer
*/
setAutoHeight: function () {
// If we are in list mode, don't determine the height of any elements because we are not "full screen"
if (
MetacatUI.appModel.get("searchMode") == "list" ||
this.fixedHeight
) {
MetacatUI.appView.$(".auto-height").height("auto");
return;
}
// Get the heights of the header, navbar, and footer
var otherHeight = 0;
$(".auto-height-member").each(function (i, el) {
if ($(el).css("display") != "none") {
otherHeight += $(el).outerHeight(true);
}
});
// Get the remaining height left based on the window size
var remainingHeight = $(window).outerHeight(true) - otherHeight;
if (remainingHeight < 0)
remainingHeight = $(window).outerHeight(true) || 300;
else if (remainingHeight <= 120)
remainingHeight =
$(window).outerHeight(true) - remainingHeight || 300;
// Adjust all elements with the .auto-height class
$(".auto-height").height(remainingHeight);
if (
$("#map-container.auto-height").length > 0 &&
$("#map-canvas").length > 0
) {
var otherHeight = 0;
$("#map-container.auto-height")
.children()
.each(function (i, el) {
if ($(el).attr("id") != "map-canvas") {
otherHeight += $(el).outerHeight(true);
}
});
var newMapHeight = remainingHeight - otherHeight;
if (newMapHeight > 100) {
$("#map-canvas").height(remainingHeight - otherHeight);
}
}
// Trigger a resize for the map so that all of the map background images are loaded
if (gmaps && this.mapModel && this.mapModel.get("map")) {
google.maps.event.trigger(this.mapModel.get("map"), "resize");
}
},
/*
* ==================================================================================================
* PERFORMING SEARCH
* ==================================================================================================
*/
triggerSearch: function () {
// Set the sort order
var sortOrder = $("#sortOrder").val();
if (sortOrder) {
this.searchModel.set("sortOrder", sortOrder);
}
// Trigger a search to load the results
MetacatUI.appModel.trigger("search");
if (!this.isSubView) {
// make sure the browser knows where we are
var route = Backbone.history.fragment;
if (route.indexOf("data") < 0) {
MetacatUI.uiRouter.navigate("data", {
trigger: false,
replace: true,
});
} else {
MetacatUI.uiRouter.navigate(route);
}
}
// ...but don't want to follow links
return false;
},
triggerOnEnter: function (e) {
if (e.keyCode != 13) return;
// Update the filters
this.updateTextFilters(e);
},
/**
* getResults gets all the current search filters from the searchModel, creates a Solr query, and runs that query.
* @param {number} page - The page of search results to get results for
*/
getResults: function (page) {
// Set the sort order based on user choice
var sortOrder = this.searchModel.get("sortOrder");
if (sortOrder) {
this.searchResults.setSort(sortOrder);
}
// Specify which fields to retrieve
var fields = "";
fields += "id,";
fields += "seriesId,";
fields += "title,";
fields += "origin,";
fields += "pubDate,";
fields += "dateUploaded,";
fields += "abstract,";
fields += "resourceMap,";
fields += "beginDate,";
fields += "endDate,";
fields += "read_count_i,";
fields += "geohash_9,";
fields += "datasource,";
fields += "isPublic,";
fields += "documents,";
fields += "sem_annotation,";
// Add spatial fields if the map is present
if (gmaps) {
fields += "northBoundCoord,";
fields += "southBoundCoord,";
fields += "eastBoundCoord,";
fields += "westBoundCoord";
}
// Strip the last trailing comma if needed
if (fields[fields.length - 1] === ",") {
fields = fields.substr(0, fields.length - 1);
}
this.searchResults.setfields(fields);
// Get the query
var query = this.searchModel.getQuery();
// Specify which facets to retrieve
if (gmaps && this.map) {
// If we have Google Maps enabled
var geohashLevel =
"geohash_" + this.mapModel.determineGeohashLevel(this.map.zoom);
this.searchResults.facet.push(geohashLevel);
}
// Run the query
this.searchResults.setQuery(query);
// Get the page number
if (this.isSubView) {
var page = 0;
} else {
var page = MetacatUI.appModel.get("page");
if (page == null) {
page = 0;
}
}
this.searchResults.start = page * this.searchResults.rows;
// Show or hide the reset filters button
this.toggleClearButton();
// go to the page
this.showPage(page);
// don't want to follow links
return false;
},
/*
* After the search results have been returned,
* check if any of them are derived data or have derivations
*/
checkForProv: function () {
var maps = [],
hasSources = [],
hasDerivations = [],
mainSearchResults = this.searchResults;
// Get a list of all the resource map IDs from the SolrResults collection
maps = this.searchResults.pluck("resourceMap");
maps = _.compact(_.flatten(maps));
// Create a new Search model with a search that finds all members of these packages/resource maps
var provSearchModel = new SearchModel({
formatType: [
{
value: "DATA",
label: "data",
description: null,
},
],
exclude: [],
resourceMap: maps,
});
// Create a new Solr Results model to store the results of this supplemental query
var provSearchResults = new SearchResults(null, {
query: provSearchModel.getQuery(),
searchLogs: false,
usePOST: true,
rows: 150,
fields: provSearchModel.getProvFlList() + ",id,resourceMap",
});
// Trigger a search on that Solr Results model
this.listenTo(provSearchResults, "reset", function (results) {
if (results.models.length == 0) return;
// See if any of the results have a value for a prov field
results.forEach(function (result) {
if (!result.getSources().length || !result.getDerivations()) return;
_.each(result.get("resourceMap"), function (rMapID) {
if (_.contains(maps, rMapID)) {
var match = mainSearchResults.filter(
function (mainSearchResult) {
return _.contains(
mainSearchResult.get("resourceMap"),
rMapID,
);
},
);
if (match && match.length && result.getSources().length > 0)
hasSources.push(match[0].get("id"));
if (match && match.length && result.getDerivations().length > 0)
hasDerivations.push(match[0].get("id"));
}
});
});
// Filter out the duplicates
hasSources = _.uniq(hasSources);
hasDerivations = _.uniq(hasDerivations);
// If they do, find their corresponding result row here and add
// the prov icon (or just change the class to active)
_.each(hasSources, function (metadataID) {
var metadataDoc = mainSearchResults.findWhere({
id: metadataID,
});
if (metadataDoc) {
metadataDoc.set("prov_hasSources", true);
}
});
_.each(hasDerivations, function (metadataID) {
var metadataDoc = mainSearchResults.findWhere({
id: metadataID,
});
if (metadataDoc) {
metadataDoc.set("prov_hasDerivations", true);
}
});
});
provSearchResults.toPage(0);
},
cacheSearch: function () {
MetacatUI.appModel.get("searchHistory").push({
search: this.searchModel.clone(),
map: this.mapModel ? this.mapModel.clone() : null,
});
MetacatUI.appModel.trigger("change:searchHistory");
},
/*
* ==================================================================================================
* FILTERS
* ==================================================================================================
*/
updateCheckboxFilter: function (e, category, value) {
if (!this.filters) return;
var checkbox = e.target;
var checked = $(checkbox).prop("checked");
if (typeof category == "undefined")
var category = $(checkbox).attr("data-category");
if (typeof value == "undefined") var value = $(checkbox).attr("value");
// If the user just unchecked the box, then remove this filter
if (!checked) {
this.searchModel.removeFromModel(category, value);
this.hideFilter(category, value);
}
// If the user just checked the box, then add this filter
else {
var currentValue = this.searchModel.get(category);
// Get the description
var desc =
$(checkbox).attr("data-description") || $(checkbox).attr("title");
if (typeof desc == "undefined" || !desc) desc = "";
// Get the label
var labl = $(checkbox).attr("data-label");
if (typeof labl == "undefined" || !labl) labl = "";
// Make the filter object
var filter = {
description: desc,
label: labl,
value: value,
};
// If this filter category is an array, add this value to the array
if (Array.isArray(currentValue)) {
currentValue.push(filter);
this.searchModel.set(category, currentValue);
this.searchModel.trigger("change:" + category);
} else {
// If it isn't an array, then just update the model with a simple value
this.searchModel.set(category, filter);
}
// Show the filter element
this.showFilter(category, value, true, labl);
// Show the reset button
this.showClearButton();
}
// Route to page 1
this.updatePageNumber(0);
// Trigger a new search
this.triggerSearch();
},
updateBooleanFilters: function (e) {
if (!this.filters) return;
// Get the category
var checkbox = e.target;
var category = $(checkbox).attr("data-category");
var currentValue = this.searchModel.get(category);
// If this filter is not enabled, exit this function
if (
!_.contains(MetacatUI.appModel.get("defaultSearchFilters"), category)
) {
return false;
}
//The year filter is handled in a different way
if (category == "pubYear" || category == "dataYear") return;
// If the checkbox has a value, then update as a string value not boolean
var value = $(checkbox).attr("value");
if (value) {
this.updateCheckboxFilter(e, category, value);
return;
} else value = $(checkbox).prop("checked");
this.searchModel.set(category, value);
// Add the filter to the UI
if (value) {
this.showFilter(category, "", true);
} else {
// Remove the filter from the UI
value = "";
this.hideFilter(category, value);
}
// Show the reset button
this.showClearButton();
// Route to page 1
this.updatePageNumber(0);
// Trigger a new search
this.triggerSearch();
// Track this event
MetacatUI.analytics?.trackEvent("search", "filter, " + category, value);
},
// Update the UI year slider and input values
// Also update the model
updateYearRange: function (e) {
if (!this.filters) return;
var viewRef = this,
userAction = !(typeof e === "undefined"),
model = this.searchModel,
pubYearChecked = $("#publish_year").prop("checked"),
dataYearChecked = $("#data_year").prop("checked");
// If the year range slider has not been created yet
if (!userAction && !$("#year-range").hasClass("ui-slider")) {
var defaultMin =
typeof this.searchModel.defaults == "function"
? this.searchModel.defaults().yearMin
: 1800,
defaultMax =
typeof this.searchModel.defaults == "function"
? this.searchModel.defaults().yearMax
: new Date().getUTCFullYear();
//jQueryUI slider
$("#year-range").slider({
range: true,
disabled: false,
min: defaultMin, //sets the minimum on the UI slider on initialization
max: defaultMax, //sets the maximum on the UI slider on initialization
values: [
this.searchModel.get("yearMin"),
this.searchModel.get("yearMax"),
], //where the left and right slider handles are
stop: function (event, ui) {
// When the slider is changed, update the input values
$("#min_year").val(ui.values[0]);
$("#max_year").val(ui.values[1]);
// Also update the search model
model.set("yearMin", ui.values[0]);
model.set("yearMax", ui.values[1]);
// If neither the publish year or data coverage year are checked
if (
!$("#publish_year").prop("checked") &&
!$("#data_year").prop("checked")
) {
// We want to check the data coverage year on the user's behalf
$("#data_year").prop("checked", "true");
// And update the search model
model.set("dataYear", true);
}
// Add the filter elements
if ($("#publish_year").prop("checked")) {
viewRef.showFilter(
$("#publish_year").attr("data-category"),
true,
false,
ui.values[0] + " to " + ui.values[1],
{
replace: true,
},
);
}
if ($("#data_year").prop("checked")) {
viewRef.showFilter(
$("#data_year").attr("data-category"),
true,
false,
ui.values[0] + " to " + ui.values[1],
{
replace: true,
},
);
}
// Route to page 1
viewRef.updatePageNumber(0);
// Trigger a new search
viewRef.triggerSearch();
},
});
// Get the minimum and maximum years of this current search and use those as the min and max values in the slider
this.statsModel.set("query", this.searchModel.getQuery());
this.listenTo(this.statsModel, "change:firstBeginDate", function () {
if (
this.statsModel.get("firstBeginDate") == 0 ||
!this.statsModel.get("firstBeginDate")
) {
$("#year-range").slider({
min: defaultMin,
});
return;
}
var year = new Date(
this.statsModel.get("firstBeginDate"),
).getUTCFullYear();
if (typeof year !== "undefined") {
$("#min_year").val(year);
$("#year-range").slider({
values: [year, $("#max_year").val()],
});
// If the slider min is still at the default value, then update with the min value found at this search
if ($("#year-range").slider("option", "min") == defaultMin) {
$("#year-range").slider({
min: year,
});
}
// Add the filter elements if this is set
if (viewRef.searchModel.get("pubYear")) {
viewRef.showFilter(
"pubYear",
true,
false,
$("#min_year").val() + " to " + $("#max_year").val(),
{
replace: true,
},
);
}
if (viewRef.searchModel.get("dataYear")) {
viewRef.showFilter(
"dataYear",
true,
false,
$("#min_year").val() + " to " + $("#max_year").val(),
{
replace: true,
},
);
}
}
});
// Only when the first begin date is retrieved, set the slider min and max values
this.listenTo(this.statsModel, "change:lastEndDate", function () {
if (
this.statsModel.get("lastEndDate") == 0 ||
!this.statsModel.get("lastEndDate")
) {
$("#year-range").slider({
max: defaultMax,
});
return;
}
var year = new Date(
this.statsModel.get("lastEndDate"),
).getUTCFullYear();
if (typeof year !== "undefined") {
$("#max_year").val(year);
$("#year-range").slider({
values: [$("#min_year").val(), year],
});
// If the slider max is still at the default value, then update with the max value found at this search
if ($("#year-range").slider("option", "max") == defaultMax) {
$("#year-range").slider({
max: year,
});
}
// Add the filter elements if this is set
if (viewRef.searchModel.get("pubYear")) {
viewRef.showFilter(
"pubYear",
true,
false,
$("#min_year").val() + " to " + $("#max_year").val(),
{
replace: true,
},
);
}
if (viewRef.searchModel.get("dataYear")) {
viewRef.showFilter(
"dataYear",
true,
false,
$("#min_year").val() + " to " + $("#max_year").val(),
{
replace: true,
},
);
}
}
});
this.statsModel.getFirstBeginDate();
this.statsModel.getLastEndDate();
}
// If the year slider has been created and the user initiated a new search using other filters
else if (
!userAction &&
!this.searchModel.get("dataYear") &&
!this.searchModel.get("pubYear")
) {
// Reset the min and max year based on this search
this.statsModel.set("query", this.searchModel.getQuery());
this.statsModel.getFirstBeginDate();
this.statsModel.getLastEndDate();
}
// If either of the year type selectors is what brought us here, then determine whether the user
// is completely removing both (reset both year filters) or just one (remove just that one filter)
else if (userAction) {
// When both year types were unchecked, assume user wants to reset the year filter
if (
($(e.target).attr("id") == "data_year" ||
$(e.target).attr("id") == "publish_year") &&
!pubYearChecked &&
!dataYearChecked
) {
// Reset the search model
this.searchModel.set("yearMin", defaultMin);
this.searchModel.set("yearMax", defaultMax);
this.searchModel.set("dataYear", false);
this.searchModel.set("pubYear", false);
// Reset the min and max year based on this search
this.statsModel.set("query", this.searchModel.getQuery());
this.statsModel.getFirstBeginDate();
this.statsModel.getLastEndDate();
// Slide the handles back to the defaults
$("#year-range").slider("values", [defaultMin, defaultMax]);
// Hide the filters
this.hideFilter("dataYear");
this.hideFilter("pubYear");
}
// If either of the year inputs have changed or if just one of the year types were unchecked
else {
var minVal = $("#min_year").val();
var maxVal = $("#max_year").val();
// Update the search model to match what is in the text inputs
this.searchModel.set("yearMin", minVal);
this.searchModel.set("yearMax", maxVal);
this.searchModel.set("dataYear", dataYearChecked);
this.searchModel.set("pubYear", pubYearChecked);
// If neither the publish year or data coverage year are checked
if (!pubYearChecked && !dataYearChecked) {
// We want to check the data coverage year on the user's behalf
$("#data_year").prop("checked", "true");
// And update the search model
model.set("dataYear", true);
// Add the filter elements
this.showFilter(
$("#data_year").attr("data-category"),
true,
true,
minVal + " to " + maxVal,
{
replace: true,
},
);
// Track this event
MetacatUI.analytics?.trackEvent(
"search",
"filter, Data Year",
minVal + " to " + maxVal,
);
} else {
// Add the filter elements
if (pubYearChecked) {
this.showFilter(
$("#publish_year").attr("data-category"),
true,
true,
minVal + " to " + maxVal,
{
replace: true,
},
);
// Track this event
MetacatUI.analytics?.trackEvent(
"search",
"filter, Publication Year",
minVal + " to " + maxVal,
);
} else {
this.hideFilter($("#publish_year").attr("data-category"), true);
}
if (dataYearChecked) {
this.showFilter(
$("#data_year").attr("data-category"),
true,
true,
minVal + " to " + maxVal,
{
replace: true,
},
);
// Track this event
MetacatUI.analytics?.trackEvent(
"search",
"filter, Data Year",
minVal + " to " + maxVal,
);
} else {
this.hideFilter($("#data_year").attr("data-category"), true);
}
}
}
// Route to page 1
this.updatePageNumber(0);
// Trigger a new search
this.triggerSearch();
}
},
updateTextFilters: function (e, item) {
if (!this.filters) return;
// Get the search/filter category
var category = $(e.target).attr("data-category");
// Try the parent elements if not found
if (!category) {
var parents = $(e.target)
.parents()
.each(function () {
category = $(this).attr("data-category");
if (category) {
return false;
}
});
}
if (!category) {
return false;
}
// Get the input element
var input = this.$el.find("#" + category + "_input");
// Get the value of the associated input
var term = !item || !item.value ? input.val() : item.value;
var label = !item || !item.filterLabel ? null : item.filterLabel;
var filterDesc = !item || !item.desc ? null : item.desc;
// Check that something was actually entered
if (term == "" || term == " ") {
return false;
}
// Close the autocomplete box
if (e.type == "hoverautocompleteselect") {
$(input).hoverAutocomplete("close");
} else if ($(input).data("ui-autocomplete") != undefined) {
// If the autocomplete has been initialized, then close it
$(input).autocomplete("close");
}
// Get the current searchModel array for this category
var filtersArray = _.clone(this.searchModel.get(category));
if (typeof filtersArray == "undefined") {
console.error(
"The filter category '" +
category +
"' does not exist in the Search model. Not sending this search term.",
);
return false;
}
// Check if this entry is a duplicate
var duplicate = (function () {
for (var i = 0; i < filtersArray.length; i++) {
if (filtersArray[i].value === term) {
return true;
}
}
})();
if (duplicate) {
// Display a quick message
if ($("#duplicate-" + category + "-alert").length <= 0) {
$("#current-" + category + "-filters").prepend(
"<div class='alert alert-block' id='duplicate-' + category + '-alert'>" +
"You are already using that filter" +
"</div>",
);
$("#duplicate-" + category + "-alert")
.delay(2000)
.fadeOut(500, function () {
this.remove();
});
}
return false;
}
// Add the new entry to the array of current filters
var filter = {
value: term,
filterLabel: label,
label: label,
description: filterDesc,
};
filtersArray.push(filter);
// Replace the current array with the new one in the search model
this.searchModel.set(category, filtersArray);
// Show the UI filter
this.showFilter(category, filter, false, label);
// Clear the input
input.val("");
// Route to page 1
this.updatePageNumber(0);
// Trigger a new search
this.triggerSearch();
// Track this event
MetacatUI.analytics?.trackEvent("search", "filter, " + category, term);
},
// Removes a specific filter term from the searchModel
removeFilter: function (e) {
// Get the parent element that stores the filter term
var filterNode = $(e.target).parent();
// Find this filter's category and value
var category =
filterNode.attr("data-category") ||
filterNode.parent().attr("data-category"),
value = $(filterNode).attr("data-term");
// Remove this filter from the searchModel
this.searchModel.removeFromModel(category, value);
// Hide the filter from the UI
this.hideFilter(category, value);
// If there is an associated checkbox with this filter, uncheck it
var assocCheckbox,
checkboxes = this.$(
"input[type='checkbox'][data-category='" + category + "']",
);
//If there are more than one checkboxes in this category, match by value
if (checkboxes.length > 1) {
assocCheckbox = _.find(checkboxes, function (checkbox) {
return $(checkbox).val() == value;
});
}
//If there is only one checkbox in this category, default to it
else if (checkboxes.length == 1) {
assocCheckbox = checkboxes[0];
}
//If there is an associated checkbox, uncheck it
if (assocCheckbox) {
//Uncheck it
$(assocCheckbox).prop("checked", false);
}
// Route to page 1
this.updatePageNumber(0);
// Trigger a new search
this.triggerSearch();
},
// Clear all the currently applied filters
resetFilters: function () {
var viewRef = this;
this.allowSearch = true;
// Hide all the filters in the UI
$.each(this.$(".current-filter"), function () {
viewRef.hideEl(this);
});
// Hide the clear button
this.hideClearButton();
// Then reset the model
this.searchModel.clear();
//Reset the map model
if (this.mapModel) {
this.mapModel.clear();
}
// Reset the year slider handles
$("#year-range").slider("values", [
this.searchModel.get("yearMin"),
this.searchModel.get("yearMax"),
]);
//and the year inputs
$("#min_year").val(this.searchModel.get("yearMin"));
$("#max_year").val(this.searchModel.get("yearMax"));
// Reset the checkboxes
$("#includes_data").prop("checked", this.searchModel.get("documents"));
$("#data_year").prop("checked", this.searchModel.get("dataYear"));
$("#publish_year").prop("checked", this.searchModel.get("pubYear"));
$("#is_private_data").prop(
"checked",
this.searchModel.get("isPrivate"),
);
this.listDataSources();
// Zoom out the Google Map
this.resetMap();
this.renderMap();
// Route to page 1
this.updatePageNumber(0);
// Trigger a new search
this.triggerSearch();
},
hideEl: function (element) {
// Fade out and remove the element
$(element).fadeOut("slow", function () {
$(element).remove();
});
},
// Removes a specified filter node from the DOM
hideFilter: function (category, value) {
if (!this.filters) return;
if (typeof value === "undefined") {
var filterNode = this.$(
".current-filters[data-category='" + category + "']",
).children(".current-filter");
} else {
var filterNode = this.$(
".current-filters[data-category='" + category + "']",
).children("[data-term='" + value + "']");
}
// Try finding it a different way
if (!filterNode || !filterNode.length) {
filterNode = this.$(
".current-filter[data-category='" + category + "']",
);
}
// Remove the filter node from the DOM
this.hideEl(filterNode);
},
// Adds a specified filter node to the DOM
showFilter: function (
category,
term,
checkForDuplicates,
label,
options,
) {
if (!this.filters) return;
var viewRef = this;
if (typeof term === "undefined") return false;
// Get the element to add the UI filter node to
// The pattern is #current-<category>-filters
var filterContainer = this.$el.find(
"#current-" + category + "-filters",
);
// Allow the option to only display this exact filter category and term once to the DOM
// Helpful when adding a filter that is not stored in the search model (for display only)
if (checkForDuplicates) {
var duplicate = false;
// Get the current terms from the DOM and check against the new term
filterContainer.children().each(function () {
if ($(this).attr("data-term") == term) {
duplicate = true;
}
});
// If there is a duplicate, exit without adding it
if (duplicate) {
return;
}
}
var value = null,
desc = null;
// See if this filter is an object and extract the filter attributes
if (typeof term === "object") {
if (typeof term.description !== "undefined") {
desc = term.description;
}
if (typeof term.filterLabel !== "undefined") {
label = term.filterLabel;
} else if (typeof term.label !== "undefined" && term.label) {
label = term.label;
} else {
label = null;
}
if (typeof term.value !== "undefined") {
value = term.value;
}
} else {
value = term;
// Find the filter label
if (typeof label === "undefined" || !label) {
// Use the filter value for the label, sans any leading # character
if (value.indexOf("#") > 0) {
label = value.substring(value.indexOf("#"));
}
}
desc = label;
}
var categoryLabel = this.searchModel.fieldLabels[category];
if (
typeof categoryLabel === "undefined" &&
category == "additionalCriteria"
)
categoryLabel = "";
if (typeof categoryLabel === "undefined") categoryLabel = category;
// Add a filter node to the DOM
var filterEl = viewRef.currentFilterTemplate({
category: Utilities.encodeHTML(categoryLabel),
value: Utilities.encodeHTML(value),
label: Utilities.encodeHTML(label),
description: Utilities.encodeHTML(desc),
});
// Add the filter to the page - either replace or tack on
if (options && options.replace) {
var currentFilter = filterContainer.find(".current-filter");
if (currentFilter.length > 0) {
currentFilter.replaceWith(filterEl);
} else {
filterContainer.prepend(filterEl);
}
} else {
filterContainer.prepend(filterEl);
}
// Tooltips and Popovers
$(filterEl).tooltip({
delay: {
show: 800,
},
});
return;
},
/*
* Get the member node list from the model and list the members in the filter list
*/
listDataSources: function () {
if (!this.filters) return;
if (MetacatUI.nodeModel.get("members").length < 1) return;
// Get the member nodes
var members = _.sortBy(
MetacatUI.nodeModel.get("members"),
function (m) {
if (m.name) {
return m.name.toLowerCase();
} else {
return "";
}
},
);
var filteredMembers = _.reject(members, function (m) {
return m.status != "operational";
});
// Get the current search filters for data source
var currentFilters = this.searchModel.get("dataSource");
// Create an HTML list
var listMax = 4,
numHidden = filteredMembers.length - listMax,
list = $(document.createElement("ul")).addClass("checkbox-list");
// Add a checkbox and label for each member node in the node model
_.each(filteredMembers, function (member, i) {
var listItem = document.createElement("li"),
input = document.createElement("input"),
label = document.createElement("label");
// If this member node is already a data source filter, then the checkbox is checked
var checked = _.findWhere(currentFilters, {
value: member.identifier,
})
? true
: false;
// Create a textual label for this data source
$(label)
.addClass("ellipsis")
.attr("for", member.identifier)
.html(member.name);
// Create a checkbox for this data source
$(input)
.addClass("filter")
.attr("type", "checkbox")
.attr("data-category", "dataSource")
.attr("id", member.identifier)
.attr("name", "dataSource")
.attr("value", member.identifier)
.attr("data-label", member.name)
.attr("data-description", member.description);
// Add tooltips to the label element
$(label).tooltip({
placement: "top",
delay: {
show: 900,
},
trigger: "hover",
viewport: "#sidebar",
title: member.description,
});
// If this data source is already selected as a filter (from the search model), then check the checkbox
if (checked) $(input).prop("checked", "checked");
// Collapse some of the checkboxes and labels after a certain amount
if (i > listMax - 1) {
$(listItem).addClass("hidden");
}
// Insert a "More" link after a certain amount to enable users to expand the list
if (i == listMax) {
var moreLink = document.createElement("a");
$(moreLink)
.html("Show " + numHidden + " more")
.addClass("more-link pointer toggle-list")
.append(
$(document.createElement("i")).addClass("icon icon-expand-alt"),
);
$(list).append(moreLink);
}
// Add this checkbox and laebl to the list
$(listItem).append(input).append(label);
$(list).append(listItem);
});
if (numHidden > 0) {
var lessLink = document.createElement("a");
$(lessLink)
.html("Collapse member nodes")
.addClass("less-link toggle-list pointer hidden")
.append(
$(document.createElement("i")).addClass("icon icon-collapse-alt"),
);
$(list).append(lessLink);
}
// Add the list of checkboxes to the placeholder
var container = $(".member-nodes-placeholder");
$(container).html(list);
$(".tooltip-this").tooltip();
},
resetDataSourceList: function () {
if (!this.filters) return;
// Reset the Member Nodes checkboxes
var mnFilterContainer = $("#member-nodes-container"),
defaultMNs = this.searchModel.get("dataSource");
// Make sure the member node filter exists
if (!mnFilterContainer || mnFilterContainer.length == 0) return false;
if (typeof defaultMNs === "undefined" || !defaultMNs) return false;
// Reset each member node checkbox
var boxes = $(mnFilterContainer).find(".filter").prop("checked", false);
// Check the member node checkboxes that are defaults in the search model
_.each(defaultMNs, function (member, i) {
var value = null;
// Allow for string search model filter values and object filter values
if (typeof member !== "object" && member) value = member;
else if (typeof member.value === "undefined" || !member.value)
value = "";
else value = member.value;
$(mnFilterContainer)
.find("checkbox[value='" + value + "']")
.prop("checked", true);
});
return true;
},
toggleList: function (e) {
if (!this.filters) return;
var link = e.target,
controls = $(link).parents("ul").find(".toggle-list"),
list = $(link).parents("ul"),
isHidden = !list.find(".more-link").is(".hidden");
// Hide/Show the list
if (isHidden) {
list.children("li").slideDown();
} else {
list.children("li.hidden").slideUp();
}
// Hide/Show the control links
controls.toggleClass("hidden");
},
// add additional criteria to the search model based on link click
additionalCriteria: function (e) {
// Get the clicked node
var targetNode = $(e.target);
// If this additional criteria is already applied, remove it
if (targetNode.hasClass("active")) {
this.removeAdditionalCriteria(e);
return false;
}
// Get the filter criteria
var term = targetNode.attr("data-term");
// Find this element's category in the data-category attribute
var category = targetNode.attr("data-category");
// style the selection
$(".keyword-search-link").removeClass("active");
$(".keyword-search-link").parent().removeClass("active");
targetNode.addClass("active");
targetNode.parent().addClass("active");
// Add this criteria to the search model
this.searchModel.set(category, [term]);
// Trigger the search
this.triggerSearch();
// prevent default action of click
return false;
},
removeAdditionalCriteria: function (e) {
// Get the clicked node
var targetNode = $(e.target);
// Reference to model
var model = this.searchModel;
// remove the styling
$(".keyword-search-link").removeClass("active");
$(".keyword-search-link").parent().removeClass("active");
// Get the term
var term = targetNode.attr("data-term");
// Get the current search model additional criteria
var current = this.searchModel.get("additionalCriteria");
// If this term is in the current search model (should be)...
if (_.contains(current, term)) {
//then remove it
var newTerms = _.without(current, term);
model.set("additionalCriteria", newTerms);
}
// Route to page 1
this.updatePageNumber(0);
// Trigger a new search
this.triggerSearch();
},
// Get the facet counts
getAutocompletes: function (e) {
if (!e) return;
// Get the text input to determine the filter type
var input = $(e.target),
category = input.attr("data-category");
if (!this.filters || !category) return;
var viewRef = this;
// Create the facet query by using our current search query
var facetQuery =
"q=" +
this.searchResults.currentquery +
"&rows=0" +
this.searchModel.getFacetQuery(category) +
"&wt=json&";
// If we've cached these filter results, then use the cache instead of sending a new request
if (!MetacatUI.appSearchModel.autocompleteCache)
MetacatUI.appSearchModel.autocompleteCache = {};
else if (MetacatUI.appSearchModel.autocompleteCache[facetQuery]) {
this.setupAutocomplete(
input,
MetacatUI.appSearchModel.autocompleteCache[facetQuery],
);
return;
}
// Get the facet counts for the autocomplete
var requestSettings = {
url: MetacatUI.appModel.get("queryServiceUrl") + facetQuery,
type: "GET",
dataType: "json",
success: function (data, textStatus, xhr) {
var suggestions = [],
facetLimit = 999;
// Get all the facet counts
_.each(category.split(","), function (c) {
if (typeof c == "string") c = [c];
_.each(c, function (thisCategory) {
// Get the field name(s)
var fieldNames =
MetacatUI.appSearchModel.facetNameMap[thisCategory];
if (typeof fieldNames == "string") fieldNames = [fieldNames];
// Get the facet counts
_.each(fieldNames, function (fieldName) {
suggestions.push(data.facet_counts.facet_fields[fieldName]);
});
});
});
suggestions = _.flatten(suggestions);
// Format the suggestions
var rankedSuggestions = new Array();
for (
var i = 0;
i < Math.min(suggestions.length - 1, facetLimit);
i += 2
) {
//The label is the item value
var label = suggestions[i];
//For all categories except the 'all' category, display the facet count
if (category != "all") {
label += " (" + suggestions[i + 1] + ")";
}
//Push the autocomplete item to the array
rankedSuggestions.push({
value: suggestions[i],
label: label,
});
}
// Save these facets in the app so we don't have to send another query
MetacatUI.appSearchModel.autocompleteCache[facetQuery] =
rankedSuggestions;
// Now setup the actual autocomplete menu
viewRef.setupAutocomplete(input, rankedSuggestions);
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
setupAutocomplete: function (input, rankedSuggestions) {
var viewRef = this;
//Override the _renderItem() function which renders a single autocomplete item.
// We want to use the 'title' HTML attribute on each item.
// This method must create a new <li> element, append it to the menu, and return it.
$.widget("custom.autocomplete", $.ui.autocomplete, {
_renderItem: function (ul, item) {
return $(document.createElement("li"))
.attr("title", item.label)
.append(item.label)
.appendTo(ul);
},
});
input.autocomplete({
source: function (request, response) {
var term = $.ui.autocomplete.escapeRegex(request.term),
startsWithMatcher = new RegExp("^" + term, "i"),
startsWith = $.grep(rankedSuggestions, function (value) {
return startsWithMatcher.test(
value.label || value.value || value,
);
}),
containsMatcher = new RegExp(term, "i"),
contains = $.grep(rankedSuggestions, function (value) {
return (
$.inArray(value, startsWith) < 0 &&
containsMatcher.test(value.label || value.value || value)
);
});
response(startsWith.concat(contains));
},
select: function (event, ui) {
// set the text field
input.val(ui.item.value);
// add to the filter immediately
viewRef.updateTextFilters(event, ui.item);
// prevent default action
return false;
},
position: {
my: "left top",
at: "left bottom",
collision: "flipfit",
},
});
},
hideClearButton: function () {
if (!this.filters) return;
// Hide the current filters panel
this.$(".current-filters-container").slideUp();
// Hide the reset button
$("#clear-all").addClass("hidden");
this.setAutoHeight();
},
showClearButton: function () {
if (!this.filters) return;
// Show the current filters panel
if (
_.difference(
this.searchModel.getCurrentFilters(),
this.searchModel.spatialFilters,
).length > 0
) {
this.$(".current-filters-container").slideDown();
}
// Show the reset button
$("#clear-all").removeClass("hidden");
this.setAutoHeight();
},
/*
* ==================================================================================================
* NAVIGATING THE UI
* ==================================================================================================
*/
// Update all the statistics throughout the page
updateStats: function () {
if (this.searchResults.header != null) {
this.$statcounts = this.$("#statcounts");
this.$statcounts.html(
this.statsTemplate({
start: this.searchResults.header.get("start") + 1,
end:
this.searchResults.header.get("start") +
this.searchResults.length,
numFound: this.searchResults.header.get("numFound"),
}),
);
}
// piggy back here
this.updatePager();
},
updatePager: function () {
if (this.searchResults.header != null) {
var pageCount = Math.ceil(
this.searchResults.header.get("numFound") /
this.searchResults.header.get("rows"),
);
// If no results were found, display a message instead of the list and clear the pagination.
if (pageCount == 0) {
this.$results.html(
"<p id='no-results-found'>No results found.</p>",
);
this.$("#resultspager").html("");
this.$(".resultspager").html("");
}
// Do not display the pagination if there is only one page
else if (pageCount == 1) {
this.$("#resultspager").html("");
this.$(".resultspager").html("");
} else {
var pages = new Array(pageCount);
// mark current page correctly, avoid NaN
var currentPage = -1;
try {
currentPage = Math.floor(
(this.searchResults.header.get("start") /
this.searchResults.header.get("numFound")) *
pageCount,
);
} catch (ex) {
console.log("Exception when calculating pages:" + ex.message);
}
// Populate the pagination element in the UI
this.$(".resultspager").html(
this.pagerTemplate({
pages: pages,
currentPage: currentPage,
}),
);
this.$("#resultspager").html(
this.pagerTemplate({
pages: pages,
currentPage: currentPage,
}),
);
}
}
},
updatePageNumber: function (page) {
MetacatUI.appModel.set("page", page);
if (!this.isSubView) {
var route = Backbone.history.fragment,
subroutePos = route.indexOf("/page/"),
newPage = parseInt(page) + 1;
//replace the last number with the new one
if (page > 0 && subroutePos > -1) {
route = route.replace(/\d+$/, newPage);
} else if (page > 0) {
route += "/page/" + newPage;
} else if (subroutePos >= 0) {
route = route.substring(0, subroutePos);
}
MetacatUI.uiRouter.navigate(route);
}
},
// Next page of results
nextpage: function () {
this.loading();
this.searchResults.nextpage();
this.$resultsview.show();
this.updateStats();
var page = MetacatUI.appModel.get("page");
page++;
this.updatePageNumber(page);
},
// Previous page of results
prevpage: function () {
this.loading();
this.searchResults.prevpage();
this.$resultsview.show();
this.updateStats();
var page = MetacatUI.appModel.get("page");
page--;
this.updatePageNumber(page);
},
navigateToPage: function (event) {
var page = $(event.target).attr("page");
this.showPage(page);
},
showPage: function (page) {
this.loading();
this.searchResults.toPage(page);
this.$resultsview.show();
this.updateStats();
this.updatePageNumber(page);
this.updateYearRange();
},
/*
* ==================================================================================================
* THE MAP
* ==================================================================================================
*/
renderMap: function () {
// If gmaps isn't enabled or loaded with an error, use list mode
if (!gmaps || this.mode == "list") {
this.ready = true;
this.mode = "list";
return;
}
if (this.isSubView) {
this.$el.addClass("mapMode");
} else {
$("body").addClass("mapMode");
}
// Get the map options and create the map
gmaps.visualRefresh = true;
var mapOptions = this.mapModel.get("mapOptions");
var defaultZoom = mapOptions.zoom;
$("#map-container").append("<div id='map-canvas'></div>");
this.map = new gmaps.Map($("#map-canvas")[0], mapOptions);
this.mapModel.set("map", this.map);
this.hasZoomed = false;
this.hasDragged = false;
// Hide the map filter toggle element
this.$(this.mapFilterToggle).hide();
// Store references
var mapRef = this.map;
var viewRef = this;
google.maps.event.addListener(mapRef, "zoom_changed", function () {
// If the map is zoomed in further than the default zoom level,
// than we want to mark the map as zoomed in
if (viewRef.map.getZoom() > defaultZoom) {
viewRef.hasZoomed = true;
}
//If we are at the default zoom level or higher, than do not mark the map
// as zoomed in
else {
viewRef.hasZoomed = false;
}
});
google.maps.event.addListener(mapRef, "dragend", function () {
viewRef.hasDragged = true;
});
google.maps.event.addListener(mapRef, "idle", function () {
// Remove all markers from the map
for (var i = 0; i < viewRef.resultMarkers.length; i++) {
viewRef.resultMarkers[i].setMap(null);
}
viewRef.resultMarkers = new Array();
//Check if the user has interacted with the map just now, and if so, we
// want to alter the geohash filter (changing the geohash values or resetting it completely)
var alterGeohashFilter =
viewRef.allowSearch || viewRef.hasZoomed || viewRef.hasDragged;
if (!alterGeohashFilter) {
return;
}
//Determine if the map needs to be recentered. The map only needs to be
// recentered if it is not at the default lat,long center point AND it
// is not zoomed in or dragged to a new center point
var setGeohashFilter =
viewRef.hasZoomed && viewRef.isMapFilterEnabled();
//If we are using the geohash filter defined by this map, then
// apply the filter and trigger a new search
if (setGeohashFilter) {
viewRef.$(viewRef.mapFilterToggle).show();
// Get the Google map bounding box
var boundingBox = mapRef.getBounds();
// Set the search model spatial filters
// Encode the Google Map bounding box into geohash
var north = boundingBox.getNorthEast().lat(),
west = boundingBox.getSouthWest().lng(),
south = boundingBox.getSouthWest().lat(),
east = boundingBox.getNorthEast().lng();
viewRef.searchModel.set("north", north);
viewRef.searchModel.set("west", west);
viewRef.searchModel.set("south", south);
viewRef.searchModel.set("east", east);
// Save the center position and zoom level of the map
viewRef.mapModel.get("mapOptions").center = mapRef.getCenter();
viewRef.mapModel.get("mapOptions").zoom = mapRef.getZoom();
// Determine the precision of geohashes to search for
var zoom = mapRef.getZoom();
var precision = viewRef.mapModel.getSearchPrecision(zoom);
// Get all the geohash tiles contained in the map bounds
var geohashBBoxes = nGeohash.bboxes(
south,
west,
north,
east,
precision,
);
// Save our geohash search settings
viewRef.searchModel.set("geohashes", geohashBBoxes);
viewRef.searchModel.set("geohashLevel", precision);
//Start back at page 0
MetacatUI.appModel.set("page", 0);
//Mark the view as ready to start a search
viewRef.ready = true;
// Trigger a new search
viewRef.triggerSearch();
viewRef.allowSearch = false;
} else {
//Reset the map filter
viewRef.resetMap();
//Start back at page 0
MetacatUI.appModel.set("page", 0);
//Mark the view as ready to start a search
viewRef.ready = true;
// Trigger a new search
viewRef.triggerSearch();
viewRef.allowSearch = false;
return;
}
});
},
// Resets the model and view settings related to the map
resetMap: function () {
if (!gmaps) {
return;
}
// First reset the model
// The categories pertaining to the map
var categories = ["east", "west", "north", "south"];
// Loop through each and remove the filters from the model
for (var i = 0; i < categories.length; i++) {
this.searchModel.set(categories[i], null);
}
// Reset the map settings
this.searchModel.resetGeohash();
this.mapModel.set("mapOptions", this.mapModel.defaults().mapOptions);
this.allowSearch = false;
},
isMapFilterEnabled: function () {
var toggleInput = this.$("input" + this.mapFilterToggle);
if (typeof toggleInput === "undefined" || !toggleInput) return;
return $(toggleInput).prop("checked");
},
toggleMapFilter: function (e, a) {
var toggleInput = this.$("input" + this.mapFilterToggle);
if (typeof toggleInput === "undefined" || !toggleInput) return;
var isOn = $(toggleInput).prop("checked");
// If the user clicked on the label, then change the checkbox for them
if (e.target.tagName != "INPUT") {
isOn = !isOn;
toggleInput.prop("checked", isOn);
}
google.maps.event.trigger(this.mapModel.get("map"), "idle");
// Track this event
MetacatUI.analytics?.trackEvent("map", isOn ? "on" : "off");
},
/**
* Show the marker, infoWindow, and bounding coordinates polygon on
the map when the user hovers on the marker icon in the result list
* @param {Event} e
*/
showResultOnMap: function (e) {
// Exit if maps are not in use
if (this.mode != "map" || !gmaps) {
return false;
}
// Get the attributes about this dataset
var resultRow = e.target,
id = $(resultRow).attr("data-id");
// The mouseover event might be triggered by a nested element, so loop through the parents to find the id
if (typeof id == "undefined") {
$(resultRow)
.parents()
.each(function () {
if (typeof $(this).attr("data-id") != "undefined") {
id = $(this).attr("data-id");
resultRow = this;
}
});
}
// Find the tile for this data set and highlight it on the map
var resultGeohashes = this.searchResults
.findWhere({
id: id,
})
.get("geohash_9");
for (var i = 0; i < resultGeohashes.length; i++) {
var thisGeohash = resultGeohashes[i],
latLong = nGeohash.decode(thisGeohash),
position = new google.maps.LatLng(
latLong.latitude,
latLong.longitude,
),
containingTileGeohash = _.find(this.tileGeohashes, function (g) {
return thisGeohash.indexOf(g) == 0;
}),
containingTile = _.findWhere(this.tiles, {
geohash: containingTileGeohash,
});
// If this is a geohash for a georegion outside the map, do not highlight a tile or display a marker
if (typeof containingTile === "undefined") continue;
this.highlightTile(containingTile);
// Set up the options for each marker
var markerOptions = {
position: position,
icon: this.mapModel.get("markerImage"),
zIndex: 99999,
map: this.map,
};
// Create the marker and add to the map
var marker = new google.maps.Marker(markerOptions);
this.resultMarkers.push(marker);
}
},
/**
* Hide the marker, infoWindow, and bounding coordinates polygon on
the map when the user stops hovering on the marker icon in the result list
* @param {Event} e - The event that brought us to this function
*/
hideResultOnMap: function (e) {
// Exit if maps are not in use
if (this.mode != "map" || !gmaps) {
return false;
}
// Get the attributes about this dataset
var resultRow = e.target,
id = $(resultRow).attr("data-id");
// The mouseover event might be triggered by a nested element, so loop through the parents to find the id
if (typeof id == "undefined") {
$(e.target)
.parents()
.each(function () {
if (typeof $(this).attr("data-id") != "undefined") {
id = $(this).attr("data-id");
resultRow = this;
}
});
}
// Get the map tile for this result and un-highlight it
var resultGeohashes = this.searchResults
.findWhere({
id: id,
})
.get("geohash_9");
for (var i = 0; i < resultGeohashes.length; i++) {
var thisGeohash = resultGeohashes[i],
containingTileGeohash = _.find(this.tileGeohashes, function (g) {
return thisGeohash.indexOf(g) == 0;
}),
containingTile = _.findWhere(this.tiles, {
geohash: containingTileGeohash,
});
// If this is a geohash for a georegion outside the map, do not unhighlight a tile
if (typeof containingTile === "undefined") continue;
// Unhighlight the tile
this.unhighlightTile(containingTile);
}
// Remove all markers from the map
_.each(this.resultMarkers, function (marker) {
marker.setMap(null);
});
this.resultMarkers = new Array();
},
/**
* Create a tile for each geohash facet. A separate tile label is added to the map with the count of the facet.
**/
drawTiles: function () {
// Exit if maps are not in use
if (this.mode != "map" || !gmaps) {
return false;
}
TextOverlay.prototype = new google.maps.OverlayView();
function TextOverlay(options) {
// Now initialize all properties.
this.bounds_ = options.bounds;
this.map_ = options.map;
this.text = options.text;
this.color = options.color;
var length = options.text.toString().length;
if (length == 1) this.width = 8;
else if (length == 2) this.width = 17;
else if (length == 3) this.width = 25;
else if (length == 4) this.width = 32;
else if (length == 5) this.width = 40;
// We define a property to hold the image's div. We'll
// actually create this div upon receipt of the onAdd()
// method so we'll leave it null for now.
this.div_ = null;
// Explicitly call setMap on this overlay
this.setMap(options.map);
}
TextOverlay.prototype.onAdd = function () {
// Create the DIV and set some basic attributes.
var div = document.createElement("div");
div.style.color = this.color;
div.style.fontSize = "15px";
div.style.position = "absolute";
div.style.zIndex = "999";
div.style.fontWeight = "bold";
// Create an IMG element and attach it to the DIV.
div.innerHTML = this.text;
// Set the overlay's div_ property to this DIV
this.div_ = div;
// We add an overlay to a map via one of the map's panes.
// We'll add this overlay to the overlayLayer pane.
var panes = this.getPanes();
panes.overlayLayer.appendChild(div);
};
TextOverlay.prototype.draw = function () {
// Size and position the overlay. We use a southwest and northeast
// position of the overlay to peg it to the correct position and size.
// We need to retrieve the projection from this overlay to do this.
var overlayProjection = this.getProjection();
// Retrieve the southwest and northeast coordinates of this overlay
// in latlngs and convert them to pixels coordinates.
// We'll use these coordinates to resize the DIV.
var sw = overlayProjection.fromLatLngToDivPixel(
this.bounds_.getSouthWest(),
);
var ne = overlayProjection.fromLatLngToDivPixel(
this.bounds_.getNorthEast(),
);
// Resize the image's DIV to fit the indicated dimensions.
var div = this.div_;
var width = this.width;
var height = 20;
div.style.left = sw.x - width / 2 + "px";
div.style.top = ne.y - height / 2 + "px";
div.style.width = width + "px";
div.style.height = height + "px";
div.style.width = width + "px";
div.style.height = height + "px";
};
TextOverlay.prototype.onRemove = function () {
this.div_.parentNode.removeChild(this.div_);
this.div_ = null;
};
// Determine the geohash level we will use to draw tiles
var currentZoom = this.map.getZoom(),
geohashLevelNum = this.mapModel.determineGeohashLevel(currentZoom),
geohashLevel = "geohash_" + geohashLevelNum,
geohashes = this.searchResults.facetCounts[geohashLevel];
// Save the current geohash level in the map model
this.mapModel.set("tileGeohashLevel", geohashLevelNum);
// Get all the geohashes contained in the map
var mapBBoxes = _.flatten(
_.values(this.searchModel.get("geohashGroups")),
);
// Geohashes may be returned that are part of datasets with multiple geographic areas. Some of these may be outside this map.
// So we will want to filter out geohashes that are not contained in this map.
if (mapBBoxes.length == 0) {
var filteredTileGeohashes = geohashes;
} else if (geohashes) {
var filteredTileGeohashes = [];
for (var i = 0; i < geohashes.length - 1; i += 2) {
// Get the geohash for this tile
var tileGeohash = geohashes[i],
isInsideMap = false,
index = 0,
searchString = tileGeohash;
// Find if any of the bounding boxes/geohashes inside our map contain this tile geohash
while (!isInsideMap && searchString.length > 0) {
searchString = tileGeohash.substring(
0,
tileGeohash.length - index,
);
if (_.contains(mapBBoxes, searchString)) isInsideMap = true;
index++;
}
if (isInsideMap) {
filteredTileGeohashes.push(tileGeohash);
filteredTileGeohashes.push(geohashes[i + 1]);
}
}
}
//If there are no tiles on the page, the map may have failed to render, so exit.
if (
typeof filteredTileGeohashes == "undefined" ||
!filteredTileGeohashes.length
) {
return;
}
// Make a copy of the array that is geohash counts only
var countsOnly = [];
for (var i = 1; i < filteredTileGeohashes.length; i += 2) {
countsOnly.push(filteredTileGeohashes[i]);
}
// Create a range of lightness to make different colors on the tiles
var lightnessMin = this.mapModel.get("tileLightnessMin"),
lightnessMax = this.mapModel.get("tileLightnessMax"),
lightnessRange = lightnessMax - lightnessMin;
// Get some stats on our tile counts so we can normalize them to create a color scale
var findMedian = function (nums) {
if (nums.length % 2 == 0) {
return (nums[nums.length / 2 - 1] + nums[nums.length / 2]) / 2;
} else {
return nums[nums.length / 2 - 0.5];
}
};
var sortedCounts = countsOnly.sort(function (a, b) {
return a - b;
}),
maxCount = sortedCounts[sortedCounts.length - 1],
minCount = sortedCounts[0];
var viewRef = this;
// Now draw a tile for each geohash facet
for (var i = 0; i < filteredTileGeohashes.length - 1; i += 2) {
// Convert this geohash to lat,long values
var tileGeohash = filteredTileGeohashes[i],
decodedGeohash = nGeohash.decode(tileGeohash),
latLngCenter = new google.maps.LatLng(
decodedGeohash.latitude,
decodedGeohash.longitude,
),
geohashBox = nGeohash.decode_bbox(tileGeohash),
swLatLng = new google.maps.LatLng(geohashBox[0], geohashBox[1]),
neLatLng = new google.maps.LatLng(geohashBox[2], geohashBox[3]),
bounds = new google.maps.LatLngBounds(swLatLng, neLatLng),
tileCount = filteredTileGeohashes[i + 1],
drawMarkers = this.mapModel.get("drawMarkers"),
marker,
count,
color;
// Normalize the range of tiles counts and convert them to a lightness domain of 20-70% lightness.
if (maxCount - minCount == 0) {
var lightness = lightnessRange;
} else {
var lightness =
((tileCount - minCount) / (maxCount - minCount)) *
lightnessRange +
lightnessMin;
}
var color =
"hsl(" + this.mapModel.get("tileHue") + "," + lightness + "%,50%)";
// Add the count to the tile
var countLocation = new google.maps.LatLngBounds(
latLngCenter,
latLngCenter,
);
// Draw the tile label with the dataset count
count = new TextOverlay({
bounds: countLocation,
map: this.map,
text: tileCount,
color: this.mapModel.get("tileLabelColor"),
});
// Set up the default tile options
var tileOptions = {
fillColor: color,
strokeColor: color,
map: this.map,
visible: true,
bounds: bounds,
};
// Merge these options with any tile options set in the map model
var modelTileOptions = this.mapModel.get("tileOptions");
for (var attr in modelTileOptions) {
tileOptions[attr] = modelTileOptions[attr];
}
// Draw this tile
var tile = this.drawTile(tileOptions, tileGeohash, count);
// Save the geohashes for tiles in the view for later
this.tileGeohashes.push(tileGeohash);
}
// Create an info window for each marker that is on the map, to display when it is clicked on
if (this.markerGeohashes.length > 0) this.addMarkers();
// If the map is zoomed all the way in, draw info windows for each tile that will be displayed when they are clicked on
if (this.mapModel.isMaxZoom(this.map)) this.addTileInfoWindows();
},
/**
* With the options and label object given, add a single tile to the map and set its event listeners
* @param {object} options
* @param {string} geohash
* @param {string} label
**/
drawTile: function (options, geohash, label) {
// Exit if maps are not in use
if (this.mode != "map" || !gmaps) {
return false;
}
// Add the tile for these datasets to the map
var tile = new google.maps.Rectangle(options);
var viewRef = this;
// Save our tiles in the view
var tileObject = {
text: label,
shape: tile,
geohash: geohash,
options: options,
};
this.tiles.push(tileObject);
// Change styles when the tile is hovered on
google.maps.event.addListener(tile, "mouseover", function (event) {
viewRef.highlightTile(tileObject);
});
// Change the styles back after the tile is hovered on
google.maps.event.addListener(tile, "mouseout", function (event) {
viewRef.unhighlightTile(tileObject);
});
// If we are at the max zoom, we will display an info window. If not, we will zoom in.
if (!this.mapModel.isMaxZoom(viewRef.map)) {
/** Set up some helper functions for zooming in on the map **/
var myFitBounds = function (myMap, bounds) {
myMap.fitBounds(bounds); // calling fitBounds() here to center the map for the bounds
var overlayHelper = new google.maps.OverlayView();
overlayHelper.draw = function () {
if (!this.ready) {
var extraZoom = getExtraZoom(
this.getProjection(),
bounds,
myMap.getBounds(),
);
if (extraZoom > 0) {
myMap.setZoom(myMap.getZoom() + extraZoom);
}
this.ready = true;
google.maps.event.trigger(this, "ready");
}
};
overlayHelper.setMap(myMap);
};
var getExtraZoom = function (
projection,
expectedBounds,
actualBounds,
) {
// in: LatLngBounds bounds -> out: height and width as a Point
var getSizeInPixels = function (bounds) {
var sw = projection.fromLatLngToContainerPixel(
bounds.getSouthWest(),
);
var ne = projection.fromLatLngToContainerPixel(
bounds.getNorthEast(),
);
return new google.maps.Point(
Math.abs(sw.y - ne.y),
Math.abs(sw.x - ne.x),
);
};
var expectedSize = getSizeInPixels(expectedBounds),
actualSize = getSizeInPixels(actualBounds);
if (
Math.floor(expectedSize.x) == 0 ||
Math.floor(expectedSize.y) == 0
) {
return 0;
}
var qx = actualSize.x / expectedSize.x;
var qy = actualSize.y / expectedSize.y;
var min = Math.min(qx, qy);
if (min < 1) {
return 0;
}
return Math.floor(Math.log(min) / Math.LN2 /* = log2(min) */);
};
// Zoom in when the tile is clicked on
gmaps.event.addListener(tile, "click", function (clickEvent) {
// Change the center
viewRef.map.panTo(clickEvent.latLng);
// Get this tile's bounds
var tileBounds = tile.getBounds();
// Get the current map bounds
var mapBounds = viewRef.map.getBounds();
// Change the zoom
//viewRef.map.fitBounds(tileBounds);
myFitBounds(viewRef.map, tileBounds);
// Track this event
MetacatUI.analytics?.trackEvent(
"map",
"clickTile",
"geohash : " + tileObject.geohash,
);
});
}
return tile;
},
highlightTile: function (tile) {
// Change the tile style on hover
tile.shape.setOptions(this.mapModel.get("tileOnHover"));
// Change the label color on hover
var div = tile.text.div_;
if (div) {
div.style.color = this.mapModel.get("tileLabelColorOnHover");
tile.text.div_ = div;
$(div).css("color", this.mapModel.get("tileLabelColorOnHover"));
}
},
unhighlightTile: function (tile) {
// Change back the tile to it's original styling
tile.shape.setOptions(tile.options);
// Change back the label color
var div = tile.text.div_;
div.style.color = this.mapModel.get("tileLabelColor");
tile.text.div_ = div;
$(div).css("color", this.mapModel.get("tileLabelColor"));
},
/**
* Get the details on each marker
* And create an infowindow for that marker
*/
addMarkers: function () {
// Exit if maps are not in use
if (this.mode != "map" || !gmaps) {
return false;
}
// Clone the Search model
var searchModelClone = this.searchModel.clone(),
geohashLevel = this.mapModel.get("tileGeohashLevel"),
viewRef = this,
markers = this.markers;
// Change the geohash filter to match our tiles
searchModelClone.set("geohashLevel", geohashLevel);
searchModelClone.set("geohashes", this.markerGeohashes);
// Now run a query to get a list of documents that are represented by our markers
var query =
"q=" +
searchModelClone.getQuery() +
"&fl=id,title,geohash_9,abstract,geohash_" +
geohashLevel +
"&rows=1000" +
"&wt=json";
var requestSettings = {
url: MetacatUI.appModel.get("queryServiceUrl") + query,
success: function (data, textStatus, xhr) {
var docs = data.response.docs;
var uniqueGeohashes = viewRef.markerGeohashes;
// Create a marker and infoWindow for each document
_.each(docs, function (doc, key, list) {
var marker,
drawMarkersAt = [];
// Find the tile place that this document belongs to
// For each geohash value at the current geohash level for this document,
_.each(doc.geohash_9, function (geohash, key, list) {
// Loop through each unique tile location to find its match
for (var i = 0; i <= uniqueGeohashes.length; i++) {
if (uniqueGeohashes[i] == geohash.substr(0, geohashLevel)) {
drawMarkersAt.push(geohash);
uniqueGeohashes = _.without(uniqueGeohashes, geohash);
}
}
});
_.each(drawMarkersAt, function (markerGeohash, key, list) {
var decodedGeohash = nGeohash.decode(markerGeohash),
latLng = new google.maps.LatLng(
decodedGeohash.latitude,
decodedGeohash.longitude,
);
// Set up the options for each marker
var markerOptions = {
position: latLng,
icon: this.mapModel.get("markerImage"),
zIndex: 99999,
map: viewRef.map,
};
// Create the marker and add to the map
var marker = new google.maps.Marker(markerOptions);
});
});
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
/**
* Get the details on each tile - a list of ids and titles for each dataset contained in that tile
* And create an infowindow for that tile
*/
addTileInfoWindows: function () {
// Exit if maps are not in use
if (this.mode != "map" || !gmaps) {
return false;
}
// Clone the Search model
var searchModelClone = this.searchModel.clone(),
geohashLevel = this.mapModel.get("tileGeohashLevel"),
geohashName = "geohash_" + geohashLevel,
viewRef = this,
infoWindows = [];
// Change the geohash filter to match our tiles
searchModelClone.set("geohashLevel", geohashLevel);
searchModelClone.set("geohashes", this.tileGeohashes);
// Now run a query to get a list of documents that are represented by our tiles
var query =
"q=" +
searchModelClone.getQuery() +
"&fl=id,title,geohash_9," +
geohashName +
"&rows=1000" +
"&wt=json";
var requestSettings = {
url: MetacatUI.appModel.get("queryServiceUrl") + query,
success: function (data, textStatus, xhr) {
// Make an infoWindow for each doc
var docs = data.response.docs;
// For each tile, loop through the docs to find which ones to include in its infoWindow
_.each(viewRef.tiles, function (tile, key, list) {
var infoWindowContent = "";
_.each(docs, function (doc, key, list) {
var docGeohashes = doc[geohashName];
if (docGeohashes) {
// Is this document in this tile?
for (var i = 0; i < docGeohashes.length; i++) {
if (docGeohashes[i] == tile.geohash) {
// Add this doc to the infoWindow content
infoWindowContent +=
"<a href='" +
MetacatUI.root +
"/view/" +
encodeURIComponent(doc.id) +
"'>" +
doc.title +
"</a> (" +
doc.id +
") <br/>";
break;
}
}
}
});
// The center of the tile
var decodedGeohash = nGeohash.decode(tile.geohash),
tileCenter = new google.maps.LatLng(
decodedGeohash.latitude,
decodedGeohash.longitude,
);
// The infowindow
var infoWindow = new gmaps.InfoWindow({
content:
"<div class='gmaps-infowindow'>" +
"<h4> Datasets located here </h4>" +
"<p>" +
infoWindowContent +
"</p>" +
"</div>",
isOpen: false,
disableAutoPan: false,
maxWidth: 250,
position: tileCenter,
});
viewRef.tileInfoWindows.push(infoWindow);
// Zoom in when the tile is clicked on
gmaps.event.addListener(
tile.shape,
"click",
function (clickEvent) {
//--- We are at max zoom, display an infowindow ----//
if (this.mapModel.isMaxZoom(viewRef.map)) {
// Find the infowindow that belongs to this tile in the view
infoWindow.open(viewRef.map);
infoWindow.isOpen = true;
// Close all other infowindows
viewRef.closeInfoWindows(infoWindow);
}
//------ We are not at max zoom, so zoom into this tile ----//
else {
// Change the center
viewRef.map.panTo(clickEvent.latLng);
// Get this tile's bounds
var bounds = tile.shape.getBounds();
// Change the zoom
viewRef.map.fitBounds(bounds);
}
},
);
// Close the infowindow upon any click on the map
gmaps.event.addListener(viewRef.map, "click", function () {
infoWindow.close();
infoWindow.isOpen = false;
});
infoWindows[tile.geohash] = infoWindow;
});
viewRef.infoWindows = infoWindows;
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
/**
* Iterate over each infowindow that we have stored in the view and close it.
* Pass an infoWindow object to this function to keep that infoWindow open/skip it
* @param {infoWindow} - An infoWindow to keep open
*/
closeInfoWindows: function (except) {
var infoWindowLists = [this.markerInfoWindows, this.tileInfoWindows];
_.each(infoWindowLists, function (infoWindows, key, list) {
// Iterate over all the marker infowindows and close all of them except for this one
for (var i = 0; i < infoWindows.length; i++) {
if (infoWindows[i].isOpen && infoWindows[i] != except) {
// Close this info window and stop looking, since only one of each kind should be open anyway
infoWindows[i].close();
infoWindows[i].isOpen = false;
i = infoWindows.length;
}
}
});
},
/**
* Remove all the tiles and text from the map
**/
removeTiles: function () {
// Exit if maps are not in use
if (this.mode != "map" || !gmaps) {
return false;
}
// Remove the tile from the map
_.each(this.tiles, function (tile, key, list) {
if (tile.shape) tile.shape.setMap(null);
if (tile.text) tile.text.setMap(null);
});
// Reset the tile storage in the view
this.tiles = [];
this.tileGeohashes = [];
this.tileInfoWindows = [];
},
/**
* Iterate over all the markers in the view and remove them from the map and view
*/
removeMarkers: function () {
// Exit if maps are not in use
if (this.mode != "map" || !gmaps) {
return false;
}
// Remove the marker from the map
_.each(this.markers, function (marker, key, list) {
marker.marker.setMap(null);
});
// Reset the marker storage in the view
this.markers = [];
this.markerGeohashes = [];
this.markerInfoWindows = [];
},
/*
* ==================================================================================================
* ADDING RESULTS
* ==================================================================================================
*/
/** Add all items in the **SearchResults** collection
* This loads the first 25, then waits for the map to be
* fully loaded and then loads the remaining items.
* Without this delay, the app waits until all records are processed
*/
addAll: function () {
// After the map is done loading, then load the rest of the results into the list
if (this.ready) this.renderAll();
else {
var viewRef = this;
var intervalID = setInterval(function () {
if (viewRef.ready) {
clearInterval(intervalID);
viewRef.renderAll();
}
}, 500);
}
// After all the results are loaded, query for our facet counts in the background
//this.getAutocompletes();
},
renderAll: function () {
// do this first to indicate coming results
this.updateStats();
// Remove all the existing tiles on the map
this.removeTiles();
this.removeMarkers();
// Remove the loading class and styling
this.$results.removeClass("loading");
// If there are no results, display so
var numFound = this.searchResults.length;
if (numFound == 0) {
// Add a No Results Found message
this.$results.html("<p id='no-results-found'>No results found.</p>");
// Remove the loading styles from the map
if (gmaps && this.mapModel) {
$("#map-container").removeClass("loading");
}
if (MetacatUI.theme == "arctic") {
// When we get new results, check if the user is searching for their own datasets and display a message
if (
MetacatUI.appView.dataCatalogView &&
MetacatUI.appView.dataCatalogView.searchModel.getQuery() ==
MetacatUI.appUserModel.get("searchModel").getQuery() &&
!MetacatUI.appSearchResults.length
) {
$("#no-results-found").after(
"<h3>Where are my data sets?</h3><p>If you are a previous ACADIS Gateway user, " +
"you will need to take additional steps to access your data sets in the new NSF Arctic Data Center." +
"<a href='mailto:support@arcticdata.io'>Send us a message at support@arcticdata.io</a> with your old ACADIS " +
"Gateway username and your ORCID identifier (" +
MetacatUI.appUserModel.get("username") +
"), we will help.</p>",
);
}
}
return;
}
// Clear the results list before we start adding new rows
this.$results.html("");
//--First map all the results--
if (gmaps && this.mapModel) {
// Draw all the tiles on the map to represent the datasets
this.drawTiles();
// Remove the loading styles from the map
$("#map-container").removeClass("loading");
}
var pid_list = new Array();
//--- Add all the results to the list ---
for (i = 0; i < this.searchResults.length; i++) {
pid_list.push(this.searchResults.models[i].get("id"));
}
if (MetacatUI.appModel.get("displayDatasetMetrics")) {
var metricsModel = new MetricsModel({
pid_list: pid_list,
type: "catalog",
});
metricsModel.fetch();
this.metricsModel = metricsModel;
}
//--- Add all the results to the list ---
for (i = 0; i < this.searchResults.length; i++) {
var element = this.searchResults.models[i];
if (typeof element !== "undefined")
this.addOne(element, this.metricsModel);
}
// Initialize any tooltips within the result item
$(".tooltip-this").tooltip();
$(".popover-this").popover();
// Set the autoheight
this.setAutoHeight();
},
/**
* Add a single SolrResult item to the list by creating a view for it and appending its element to the DOM.
*/
addOne: function (result) {
// Get the view and package service URL's
this.$view_service = MetacatUI.appModel.get("viewServiceUrl");
this.$package_service = MetacatUI.appModel.get("packageServiceUrl");
result.set({
view_service: this.$view_service,
package_service: this.$package_service,
});
var view = new SearchResultView({
model: result,
metricsModel: this.metricsModel,
});
// Add this item to the list
this.$results.append(view.render().el);
// map it
if (
gmaps &&
this.mapModel &&
typeof result.get("geohash_9") != "undefined" &&
result.get("geohash_9") != null
) {
var title = result.get("title");
for (var i = 0; i < result.get("geohash_9").length; i++) {
var centerGeohash = result.get("geohash_9")[i],
decodedGeohash = nGeohash.decode(centerGeohash),
position = new google.maps.LatLng(
decodedGeohash.latitude,
decodedGeohash.longitude,
),
marker = new gmaps.Marker({
position: position,
icon: this.mapModel.get("markerImage"),
zIndex: 99999,
});
}
}
},
/**
* When the SearchResults collection has an error getting the results,
* show an error message instead of search results
* @param {SolrResult} model
* @param {XMLHttpRequest.response} response
*/
showError: function (model, response) {
var errorMessage = "";
var statusCode = response.status;
if (!statusCode) {
statusCode = parseInt(response.statusText);
}
if (statusCode == 500 && this.solrError500Message) {
errorMessage = this.solrError500Message;
} else {
try {
errorMessage = $(response.responseText).text();
} catch (e) {
try {
errorMessage = JSON.parse(response.responseText).error.msg;
} catch (e) {
errorMessage = "";
}
} finally {
if (typeof errorMessage == "string" && errorMessage.length) {
errorMessage = "<p>Error details: " + errorMessage + "</p>";
}
}
}
MetacatUI.appView.showAlert(
"<h4><i class='icon icon-frown'></i>" +
this.solrErrorTitle +
".</h4>" +
errorMessage,
"alert-error",
this.$results,
);
this.$results.find(".loading").remove();
},
/*
* ==================================================================================================
* STYLING THE UI
* ==================================================================================================
*/
toggleMapMode: function (e) {
if (typeof e === "object") {
e.preventDefault();
}
if (gmaps) {
$(".mapMode").toggleClass("mapMode");
}
if (this.mode == "map") {
MetacatUI.appModel.set("searchMode", "list");
this.mode = "list";
this.$("#map-canvas").detach();
this.setAutoHeight();
this.getResults();
} else if (this.mode == "list") {
MetacatUI.appModel.set("searchMode", "map");
this.mode = "map";
this.renderMap();
this.setAutoHeight();
this.getResults();
}
},
// Communicate that the page is loading
loading: function () {
$("#map-container").addClass("loading");
this.$results.addClass("loading");
this.$results.html(
this.loadingTemplate({
msg: "Searching for data...",
}),
);
},
// Toggles the collapseable filters sidebar and result list in the default theme
collapse: function (e) {
var id = $(e.target).attr("data-collapse");
$("#" + id).toggleClass("collapsed");
},
toggleFilterCollapse: function (e) {
if (typeof e !== "undefined") {
var container = $(e.target).parents(".filter-contain.collapsable");
} else {
var container = this.$(".filter-contain.collapsable");
}
// If we can't find a container, then don't do anything
if (container.length < 1) return;
// Expand
if ($(container).is(".collapsed")) {
// Toggle the visibility of the collapse/expand icons
$(container).find(".expand").hide();
$(container).find(".collapse").show();
// Cache the height of this element so we can reset it on collapse
$(container).attr("data-height", $(container).css("height"));
// Increase the height of the container to expand it
$(container).css("max-height", "3000px");
}
// Collapse
else {
// Toggle the visibility of the collapse/expand icons
$(container).find(".collapse").hide();
$(container).find(".expand").show();
// Decrease the height of the container to collapse it
if ($(container).attr("data-height")) {
$(container).css("max-height", $(container).attr("data-height"));
} else {
$(container).css("max-height", "1.5em");
}
}
$(container).toggleClass("collapsed");
},
/*
* Either hides or shows the "clear all filters" button
*/
toggleClearButton: function () {
if (this.searchModel.filterCount() > 0) {
this.showClearButton();
} else {
this.hideClearButton();
}
},
// Move the popover element up the page a bit if it runs off the bottom of the page
preventPopoverRunoff: function (e) {
// In map view only (because all elements are fixed and you can't scroll)
if (this.mode == "map") {
var viewportHeight = $("#map-container").outerHeight();
} else {
return false;
}
if ($(".popover").length > 0) {
var offset = $(".popover").offset();
var popoverHeight = $(".popover").outerHeight();
var topPosition = offset.top;
// If pixels are cut off the top of the page, readjust its vertical position
if (topPosition < 0) {
$(".popover").offset({
top: 10,
});
} else {
// Else, let's check if it is cut off at the bottom
var totalHeight = topPosition + popoverHeight;
var pixelsHidden = totalHeight - viewportHeight;
var newTopPosition = topPosition - pixelsHidden - 40;
// If pixels are cut off the bottom of the page, readjust its vertical position
if (pixelsHidden > 0) {
$(".popover").offset({
top: newTopPosition,
});
}
}
}
},
onClose: function () {
this.stopListening();
$(".DataCatalog").removeClass("DataCatalog");
$(".mapMode").removeClass("mapMode");
if (gmaps) {
// unset map mode
$("body").removeClass("mapMode");
$("#map-canvas").remove();
}
// remove everything so we don't get a flicker
this.$el.html("");
},
},
);
return DataCatalogView;
});