<% if (typeof geohash_9 !== "undefined" && geohash_9 !== null){
print(' ');
@@ -45,7 +44,6 @@
}
%>
- <% } %>
<% if(hasProv){
print('');
diff --git a/src/js/templates/search/catalogSearch.html b/src/js/templates/search/catalogSearch.html
new file mode 100644
index 000000000..ea4de5f67
--- /dev/null
+++ b/src/js/templates/search/catalogSearch.html
@@ -0,0 +1,22 @@
+
+
+
+
Hide Filters
+
+
+
+
+
+
Hide Map
+
+
+
+ Limit search to map area
+
+
+
diff --git a/src/js/themes/arctic/css/metacatui.css b/src/js/themes/arctic/css/metacatui.css
index 4c4a64bfb..d6b788c62 100644
--- a/src/js/themes/arctic/css/metacatui.css
+++ b/src/js/themes/arctic/css/metacatui.css
@@ -858,7 +858,7 @@ img[src*="gstatic.com/"], img[src*="googleapis.com/"] {
#Navbar{
position: fixed;
width: 100%;
- z-index: 1;
+ z-index: 3;
background-color: #FFF;
}
.center {
@@ -941,12 +941,12 @@ img[src*="gstatic.com/"], img[src*="googleapis.com/"] {
white-space: nowrap;
}
#Content{
- padding-top: 118px;
- /* margin-left: auto;
- margin-right: auto; */
- }
- .mapMode #Content{
- padding-top: 112px;
+ padding-top: 118px;
+ /* margin-left: auto;
+ margin-right: auto; */
+ }
+ .mapMode #Content{
+ padding-top: 112px;
}
.navbar .nav {
margin-top: 8px;
@@ -3083,3 +3083,29 @@ a {
.has-temporary-message.PortalView #Content{
padding-top: 0px;
}
+
+/******************************************
+* Catalog Search View
+********************************************/
+
+/* Free up more real estate for the catalog */
+
+.catalog-search-body .header .border-image {
+ display: none;
+}
+.catalog-search-body .header .nav {
+ margin-bottom: 0;
+ display: flex;
+ align-self: center;
+}
+
+.catalog-search-body .brand {
+ height: 45px;
+}
+
+.catalog-search-body #inner-header {
+ height: 55px;
+ align-items: center;
+ margin-left: 12px;
+ margin-right: 12px;
+}
\ No newline at end of file
diff --git a/src/js/themes/arctic/routers/router.js b/src/js/themes/arctic/routers/router.js
index 99e215cf2..a8538d046 100644
--- a/src/js/themes/arctic/routers/router.js
+++ b/src/js/themes/arctic/routers/router.js
@@ -111,38 +111,33 @@ function ($, _, Backbone) {
renderData: function (mode, query, page) {
this.routeHistory.push("data");
+ // Check for a page URL parameter
+ if(!page) MetacatUI.appModel.set("page", 0);
+ else MetacatUI.appModel.set('page', page - 1);
+
+ // Check if we are using the new CatalogSearchView
+ if(!MetacatUI.appModel.get("useDeprecatedDataCatalogView")){
+ require(["views/search/CatalogSearchView"], function(CatalogSearchView){
+ MetacatUI.appView.catalogSearchView = new CatalogSearchView({
+ initialQuery: query,
+ });
+ MetacatUI.appView.showView(MetacatUI.appView.catalogSearchView);
+ });
+ return;
+ }
- ///Check for a page URL parameter
- if((typeof page === "undefined") || !page)
- MetacatUI.appModel.set("page", 0);
- else if(page == 0)
- MetacatUI.appModel.set('page', 0);
- else
- MetacatUI.appModel.set('page', page-1);
-
- //Check for a query URL parameter
- if((typeof query !== "undefined") && query){;
+ // Check for a query URL parameter
+ if ((typeof query !== "undefined") && query) {
MetacatUI.appSearchModel.set('additionalCriteria', [query]);
}
- if(!MetacatUI.appView.dataCatalogView){
- require(['views/DataCatalogView'], function(DataCatalogView){
+ require(['views/DataCatalogView'], function(DataCatalogView){
+ if (!MetacatUI.appView.dataCatalogView) {
MetacatUI.appView.dataCatalogView = new DataCatalogView();
-
- //Check for a search mode URL parameter
- if((typeof mode !== "undefined") && mode)
- MetacatUI.appView.dataCatalogView.mode = mode;
-
- MetacatUI.appView.showView(MetacatUI.appView.dataCatalogView);
- });
- }
- else{
- //Check for a search mode URL parameter
- if((typeof mode !== "undefined") && mode)
- MetacatUI.appView.dataCatalogView.mode = mode;
-
+ }
+ if (mode) MetacatUI.appView.dataCatalogView.mode = mode;
MetacatUI.appView.showView(MetacatUI.appView.dataCatalogView);
- }
+ });
},
renderMyData: function(page){
diff --git a/src/js/themes/default/css/metacatui.css b/src/js/themes/default/css/metacatui.css
index f5d40e934..3223608f7 100644
--- a/src/js/themes/default/css/metacatui.css
+++ b/src/js/themes/default/css/metacatui.css
@@ -1376,11 +1376,11 @@ li.ui-menu-item > a:hover,
/* SEARCH PAGE CSS
-------------------------------------------------- */
- .mapMode #Content{
- max-width: 100%;
- padding-top: 57px;
- padding-left: 0px;
- }
+.mapMode #Content{
+max-width: 100%;
+padding-top: 57px;
+padding-left: 0px;
+}
/*-- Results header --*/
.result-header{
diff --git a/src/js/views/AppView.js b/src/js/views/AppView.js
index 28e769ae6..af9cd8860 100644
--- a/src/js/views/AppView.js
+++ b/src/js/views/AppView.js
@@ -373,6 +373,7 @@ define([
* @property {boolean} [options.remove] If true, the user will be able to remove the alert with a "close" icon.
* @property {boolean} [options.includeEmail] If true, the alert will include a link to the {@link AppConfig#emailContact}
* @property {string} [options.emailBody] Specify an email body to use in the email link.
+ * @returns {Element} The alert element
*/
showAlert: function () {
if (arguments.length > 1) {
@@ -443,6 +444,7 @@ define([
$(options.container).prepend(alert);
}
}
+ return alert;
},
/**
diff --git a/src/js/views/DataCatalogView.js b/src/js/views/DataCatalogView.js
index 225d078f0..9c31dac22 100644
--- a/src/js/views/DataCatalogView.js
+++ b/src/js/views/DataCatalogView.js
@@ -1,17 +1,13 @@
/*global define */
define(["jquery",
- "jqueryui",
"underscore",
"backbone",
- "bioportal",
"collections/SolrResults",
"models/Search",
- "models/Stats",
"models/MetricsModel",
"common/Utilities",
"views/SearchResultView",
"views/searchSelect/AnnotationFilterView",
- "views/maps/CesiumWidgetView",
"text!templates/search.html",
"text!templates/statCounts.html",
"text!templates/pager.html",
@@ -22,8 +18,8 @@ define(["jquery",
"nGeohash"
],
function(
- $, $ui, _, Backbone, Bioportal, SearchResults, SearchModel, StatsModel,
- MetricsModel, Utilities, SearchResultView, AnnotationFilter, CesiumWidgetView,
+ $, _, Backbone, SearchResults, SearchModel,
+ MetricsModel, Utilities, SearchResultView, AnnotationFilter,
CatalogTemplate, CountTemplate, PagerTemplate, MainContentTemplate,
CurrentFilterTemplate, LoadingTemplate, gmaps, nGeohash
) {
@@ -163,9 +159,6 @@ define(["jquery",
// and event handling to sub views
render: function () {
- // Which type of map are we rendering, Google maps or Cesium maps?
- this.mapType = MetacatUI.appModel.get("dataCatalogMap") || "google";
-
// 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)
@@ -1843,7 +1836,7 @@ define(["jquery",
renderMap: function () {
// If gmaps isn't enabled or loaded with an error, use list mode
- if (this.mapType === "google" && (!gmaps || this.mode == "list")) {
+ if (!gmaps || this.mode == "list") {
this.ready = true;
this.mode = "list";
return;
@@ -1855,31 +1848,6 @@ define(["jquery",
$("body").addClass("mapMode");
}
- // Render Cesium maps, if that is the map type rendered.
- if (this.mapType == "cesium") {
- var mapContainer = $("#map-container").append("
");
-
- var mapView = new CesiumWidgetView({
- el: mapContainer
- });
- mapView.render();
- this.map = mapView;
-
- this.mapModel.set("map", this.map);
-
- this.map.showGeohashes()
-
- // Mark the view as ready to start a search
- this.ready = true;
- this.triggerSearch();
- this.allowSearch = false;
-
- // TODO / WIP : Implement the rest of the map search features...
- return
- }
-
- // Continue with rendering Google maps, if that is configured mapType
-
// Get the map options and create the map
gmaps.visualRefresh = true;
var mapOptions = this.mapModel.get("mapOptions");
@@ -2169,12 +2137,6 @@ define(["jquery",
**/
drawTiles: function () {
- // This function is for Google maps only. The CesiumWidgetView draws its
- // own tiles.
- if (this.mapType !== "google") {
- return
- }
-
// Exit if maps are not in use
if ((this.mode != "map") || (!gmaps)) {
return false;
diff --git a/src/js/views/DataCatalogViewWithFilters.js b/src/js/views/DataCatalogViewWithFilters.js
index 161952eae..e4b6ba02e 100644
--- a/src/js/views/DataCatalogViewWithFilters.js
+++ b/src/js/views/DataCatalogViewWithFilters.js
@@ -1,680 +1,729 @@
-define(["jquery",
- "underscore",
- "backbone",
- "gmaps",
- "collections/Filters",
- "collections/SolrResults",
- "models/filters/FilterGroup",
- "models/filters/SpatialFilter",
- "models/Stats",
- "views/DataCatalogView",
- "views/filters/FilterGroupsView",
- "text!templates/dataCatalog.html",
- "nGeohash"
- ],
- function($, _, Backbone, gmaps, Filters, SearchResults, FilterGroup, SpatialFilter, Stats,
- DataCatalogView, FilterGroupsView,
- template, nGeohash) {
-
- /**
- * @class DataCatalogViewWithFilters
- * @classdesc A DataCatalogView that uses the Search collection
- * and the Filter models for managing queries rather than the
- * Search model and the filter literal objects used in the
- * parent DataCatalogView. This accommodates custom portal filters.
- * This view is deprecated and will eventually be removed in a future version (likely 3.0.0)
- * @classcategory Views
- * @extends DataCatalogView
- * @constructor
- * @deprecated
- */
- var DataCatalogViewWithFilters = DataCatalogView.extend(
- /** @lends DataCatalogViewWithFilters.prototype */{
-
- el: null,
-
- /**
- * The HTML tag name for this view element
- * @type {string}
- */
- tagName: "div",
-
- /**
- * The HTML class names for this view element
- * @type {string}
- */
- className: "data-catalog",
-
- /**
- * The primary HTML template for this view
- * @type {Underscore.template}
- */
- template: _.template(template),
-
- /**
- * A reference to the PortalEditorView
- * @type {PortalEditorView}
- */
- editorView: undefined,
-
- /**
- * The sort order for the Solr query
- * @type {string}
- */
- sortOrder: "dateUploaded+desc",
-
- /**
- * The jQuery selector for the FilterGroupsView container
- * @type {string}
- */
- filterGroupsContainer: ".filter-groups-container",
-
- /**
- * The Search model to use for creating and storing Filters and constructing
- * query strings. This property is a Search model instead of a Filters
- * collection in order to be quickly compatible with the superclass/superview,
- * DataCatalogView, which was created with the (eventually to be deprecated)
- * SearchModel. A Filters collection is set on the Search model and does most
- * of the work for creating queries.
- * @type (Search)
- */
- searchModel: undefined,
-
- /**
- * Override DataCatalogView.render() to render this view with filters
- * from the Filters collection
- */
- render: function() {
- var loadingHTML;
- var templateVars;
- var compiledEl;
- var tooltips;
- var groupedTooltips;
- var forFilterLabel = true;
- var forOtherElements = false;
- // TODO: Do we really need to cache the filters collection?
- // Reconcile this from DataCatalogView.render()
- // See https://github.com/NCEAS/metacatui/blob/19d608df9cc17ac2abee76d35feca415137c09d7/src/js/views/DataCatalogView.js#L122-L145
-
- //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);
- }
-
- // Which type of map are we rendering, Google maps or Cesium maps?
- this.mapType = MetacatUI.appModel.get("dataCatalogMap") || "google";
-
- if(!this.statsModel){
- this.statsModel = new Stats();
- }
-
- if( !this.searchResults ){
- this.searchResults = new SearchResults();
- }
-
- // Use map mode on tablets and browsers only
- if ($(window).outerWidth() <= 600) {
- this.mode = "list";
- MetacatUI.appModel.set("searchMode", "list");
- gmaps = null;
- }
-
- // If this is a subview, don't set the headerType
- if (!this.isSubView) {
- MetacatUI.appModel.set("headerType", "default");
- $("body").addClass("DataCatalog");
- } else {
- this.$el.addClass("DataCatalog");
- }
- //Populate the search template with some model attributes
- loadingHTML = this.loadingTemplate({
- msg: "Loading entries ..."
- });
-
- 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"
- }
- compiledEl =
- this.template(_.extend(this.searchModel.toJSON(), templateVars));
- this.$el.html(compiledEl);
-
- //Create and render the FilterGroupsView
- this.createFilterGroups();
-
- // 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
- tooltips = $(".tooltip-this");
-
- //Find the tooltips that are on filter labels - add a slight delay to those
- groupedTooltips = _.groupBy(tooltips, function(t) {
- return ((($(t).prop("tagName") == "LABEL") ||
- ($(t).parent().prop("tagName") == "LABEL")) &&
- ($(t).parents(".filter-container").length > 0))
- });
-
- $(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"
- });
-
- // 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);
-
- // Listen to changes in the Search model Filters to trigger a search
- this.stopListening(this.searchModel.get("filters"), "add remove update reset change");
- this.listenTo(this.searchModel.get("filters"), "add remove update reset change", this.triggerSearch);
-
- // 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;
- },
-
- /**
- * Creates UI Filter Groups and renders them in this view. UI Filter Groups
- * are custom, interactive search filter elements, grouped together in one
- * panel, section, tab, etc.
- */
- createFilterGroups: function(){
-
- //If it was already created, then exit
- if( this.filterGroupsView ){
- return;
- }
-
- //Start an array for the FilterGroups and the individual Filter models
- var filterGroups = [],
- allFilters = [];
-
- //Iterate over each default FilterGroup in the app config and create a FilterGroup model
- _.each( MetacatUI.appModel.get("defaultFilterGroups"), function(filterGroupJSON){
-
- //Create the FilterGroup model
- var filterGroup = new FilterGroup(filterGroupJSON);
-
- //Add to the array
- filterGroups.push(filterGroup);
-
- //Add the Filters to the array
- allFilters = _.union(allFilters, filterGroup.get("filters").models);
-
- }, this);
-
- //Add the filters to the Search model
- this.searchModel.get("filters").add(allFilters);
-
- //Create a FilterGroupsView
- var filterGroupsView = new FilterGroupsView({
- filterGroups: filterGroups,
- filters: this.searchModel.get("filters"),
- vertical: true,
- parentView: this,
- editorView: this.editorView
- });
-
- //Add the FilterGroupsView element to this view
- this.$(this.filterGroupsContainer).html(filterGroupsView.el);
-
- //Render the FilterGroupsView
- filterGroupsView.render();
-
- //Save a reference to the FilterGroupsView
- this.filterGroupsView = filterGroupsView;
-
- },
-
- /*
- * Get Results from the Solr index by combining the Filter query string fragments
- * in each Filter instance in the Search collection and querying Solr.
- *
- * Overrides DataCatalogView.getResults().
- */
- getResults: function() {
- var sortOrder = this.searchModel.get("sortOrder");
- var query; // The full query string
- var geohashLevel; // The geohash level to search
- var page; // The page of search results to render
- var position; // The geohash level position in the facet array
-
- // Get the Solr query string from the Search filter collection
- query = this.searchModel.get("filters").getQuery();
-
- //If the query hasn't changed since the last query that was sent, don't do anything.
- //This function may have been triggered by a change event on a filter that doesn't
- //affect the query at all
- if( query == this.searchResults.getLastQuery()){
- return;
- }
-
- if ( sortOrder ) {
- this.searchResults.setSort(sortOrder);
- }
-
- //Specify which fields to retrieve
- var fields = ["id",
- "seriesId",
- "title",
- "origin",
- "pubDate",
- "dateUploaded",
- "abstract",
- "resourceMap",
- "beginDate",
- "endDate",
- "read_count_i",
- "geohash_9",
- "datasource",
- "isPublic",
- "project",
- "documents",
- "label",
- "logo",
- "formatId"];
- // Add spatial fields if the map is present
- if ( gmaps ) {
- fields.push("northBoundCoord", "southBoundCoord", "eastBoundCoord", "westBoundCoord");
- }
- //Set the field list on the SolrResults collection as a comma-separated string
- this.searchResults.setfields(fields.join(","));
-
- // Specify which geohash level is used to return tile counts
- if ( gmaps && this.map ) {
- geohashLevel = "geohash_" +
- this.mapModel.determineGeohashLevel(this.map.zoom);
- // Does it already exist as a facet field?
- position = this.searchResults.facet.indexOf(geohashLevel);
- if ( position == -1) {
- this.searchResults.facet.push(geohashLevel);
- }
- }
-
- // Set the query on the SolrResults collection
- this.searchResults.setQuery(query);
-
- // Get the page number
- if ( this.isSubView ) {
- page = 0;
- } else {
- page = MetacatUI.appModel.get("page");
- if ( page == null ) {
- page = 0;
- }
- }
- this.searchResults.start = page * this.searchResults.rows;
-
- // go to the page, which triggers a search
- this.showPage(page);
-
- // don't want to follow links
- return false;
- },
-
- /**
- * Toggle the map filter to include or exclude it from the Solr query
- */
- toggleMapFilter: function(event) {
- 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 (event && event.target.tagName != "INPUT") {
- isOn = !isOn;
- toggleInput.prop("checked", isOn);
- }
-
- var spatialFilter = _.findWhere(this.searchModel.get("filters").models, {type: "SpatialFilter"});
-
- if (isOn) {
- this.searchModel.set("useGeohash", true);
-
- if( this.filterGroupsView && spatialFilter ){
-
- this.filterGroupsView.addCustomAppliedFilter(spatialFilter);
-
- }
-
- } else {
- this.searchModel.set("useGeohash", false);
- // Remove the spatial filter from the collection
- this.searchModel.get("filters").remove(spatialFilter);
-
- if( this.filterGroupsView && spatialFilter ){
- this.filterGroupsView.removeCustomAppliedFilter(spatialFilter);
- }
- }
-
- // Tell the map to trigger a new search and redraw tiles
- this.allowSearch = true;
- google.maps.event.trigger(this.mapModel.get("map"), "idle");
-
- // Send this event to Google Analytics
- if (MetacatUI.appModel.get("googleAnalyticsKey") && (typeof ga !== "undefined")) {
- var action = isOn ? "on" : "off";
- ga("send", "event", "map", action);
- }
- },
-
- /**
- * Overload this function with an empty function since the Clear button
- * has been moved to the FilterGroupsView
- */
- toggleClearButton: function(){},
-
- /**
- * Overload this function with an empty function since the Clear button
- * has been moved to the FilterGroupsView
- */
- hideClearButton: function(){},
-
- /**
- * Overload this function with an empty function since the Clear button
- * has been moved to the FilterGroupsView
- */
- showClearButton: function(){},
-
-
- /**
- * Toggle between map and list mode
- *
- * @param(Event) the event passed by clicking the toggle-map class button
- */
- toggleMapMode: function(event) {
-
- // Block the event from bubbling
- if (typeof event === "object") {
- event.preventDefault();
- }
-
- if (gmaps) {
- $(".mapMode").toggleClass("mapMode");
- }
-
- // Toggle the mode
- 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();
- }
- },
-
- /**
- * Reset the map to the defaults
- */
- resetMap: function() {
-
- // The spatial models registered in the filters collection
- var spatialModels;
-
- if (!gmaps) {
- return;
- }
-
- // Remove the SpatialFilter from the collection silently
- // so we don't immediately trigger a new search
- spatialModels =
- _.where(this.searchModel.get("filters").models, {type: "SpatialFilter"});
- this.searchModel.get("filters").remove(spatialModels, {"silent": true});
-
- // Reset the map options to defaults
- this.mapModel.set("mapOptions", this.mapModel.defaults().mapOptions);
- this.allowSearch = false;
- },
-
- /**
- * Render the map based on the mapModel properties and search results
- */
- 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;
- }
-
- // The spatial filter instance used to constrain the search by zoom and extent
- var spatialFilter;
-
- // The map's configuration
- var mapOptions;
-
- // The map extent
- var boundingBox;
-
- // The map bounding coordinates
- var north;
- var west;
- var south;
- var east;
-
- // The map zoom level
- var zoom;
-
- // The map geohash precision based on the zoom level
- var precision;
-
- // The geohash boxes associated with the map extent and zoom
- var geohashBBoxes;
-
- // References to the map and catalog view instances for callbacks
- var mapRef;
- var viewRef;
-
- if (this.isSubView) {
- this.$el.addClass("mapMode");
- } else {
- $("body").addClass("mapMode");
- }
-
- // Get the map options and create the map
- gmaps.visualRefresh = true;
- mapOptions = this.mapModel.get("mapOptions");
- var defaultZoom = mapOptions.zoom;
- $("#map-container").append("
");
- 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();
-
- // Get the existing spatial filter if it exists
- if (this.searchModel.get("filters") &&
- this.searchModel.get("filters")
- .where({type: "SpatialFilter"}).length > 0) {
- spatialFilter = this.searchModel.get("filters")
- .where({type: "SpatialFilter"})[0];
- } else {
- spatialFilter = new SpatialFilter();
- }
-
- // Store references
- mapRef = this.map;
- viewRef = this;
-
- // Listen to idle events on the map (at rest), and render content as needed
- 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 ){
- // Get the Google map bounding box
- boundingBox = mapRef.getBounds();
-
- // Set the search model's spatial filter properties
- // Encode the Google Map bounding box into geohash
- if ( typeof boundingBox !== "undefined") {
- north = boundingBox.getNorthEast().lat();
- west = boundingBox.getSouthWest().lng();
- south = boundingBox.getSouthWest().lat();
- east = boundingBox.getNorthEast().lng();
- }
-
- // 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
- zoom = mapRef.getZoom();
+define([
+ "jquery",
+ "underscore",
+ "backbone",
+ "gmaps",
+ "collections/Filters",
+ "collections/SolrResults",
+ "models/filters/FilterGroup",
+ "models/filters/SpatialFilter",
+ "models/Stats",
+ "views/DataCatalogView",
+ "views/filters/FilterGroupsView",
+ "text!templates/dataCatalog.html",
+ "nGeohash",
+], function (
+ $,
+ _,
+ Backbone,
+ gmaps,
+ Filters,
+ SearchResults,
+ FilterGroup,
+ SpatialFilter,
+ Stats,
+ DataCatalogView,
+ FilterGroupsView,
+ template,
+ nGeohash
+) {
+ /**
+ * @class DataCatalogViewWithFilters
+ * @classdesc A DataCatalogView that uses the Search collection and the Filter
+ * models for managing queries rather than the Search model and the filter
+ * literal objects used in the parent DataCatalogView. This accommodates
+ * custom portal filters. This view is deprecated and will eventually be
+ * removed in a future version (likely 3.0.0)
+ * @classcategory Views
+ * @extends DataCatalogView
+ * @constructor
+ * @deprecated
+ */
+ var DataCatalogViewWithFilters = DataCatalogView.extend(
+ /** @lends DataCatalogViewWithFilters.prototype */ {
+ el: null,
+
+ /**
+ * The HTML tag name for this view element
+ * @type {string}
+ */
+ tagName: "div",
+
+ /**
+ * The HTML class names for this view element
+ * @type {string}
+ */
+ className: "data-catalog",
+
+ /**
+ * The primary HTML template for this view
+ * @type {Underscore.template}
+ */
+ template: _.template(template),
+
+ /**
+ * A reference to the PortalEditorView
+ * @type {PortalEditorView}
+ */
+ editorView: undefined,
+
+ /**
+ * The sort order for the Solr query
+ * @type {string}
+ */
+ sortOrder: "dateUploaded+desc",
+
+ /**
+ * The jQuery selector for the FilterGroupsView container
+ * @type {string}
+ */
+ filterGroupsContainer: ".filter-groups-container",
+
+ /**
+ * The Search model to use for creating and storing Filters and
+ * constructing query strings. This property is a Search model instead of
+ * a Filters collection in order to be quickly compatible with the
+ * superclass/superview, DataCatalogView, which was created with the
+ * (eventually to be deprecated) SearchModel. A Filters collection is set
+ * on the Search model and does most of the work for creating queries.
+ * @type (Search)
+ */
+ searchModel: undefined,
+
+ /**
+ * Override DataCatalogView.render() to render this view with filters from
+ * the Filters collection
+ */
+ render: function () {
+ var loadingHTML;
+ var templateVars;
+ var compiledEl;
+ var tooltips;
+ var groupedTooltips;
+ var forFilterLabel = true;
+ var forOtherElements = false;
+ // TODO: Do we really need to cache the filters collection? Reconcile
+ // this from DataCatalogView.render() See
+ // https://github.com/NCEAS/metacatui/blob/19d608df9cc17ac2abee76d35feca415137c09d7/src/js/views/DataCatalogView.js#L122-L145
+
+ // 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 (!this.statsModel) {
+ this.statsModel = new Stats();
+ }
+
+ if (!this.searchResults) {
+ this.searchResults = new SearchResults();
+ }
+
+ // Use map mode on tablets and browsers only
+ if ($(window).outerWidth() <= 600) {
+ this.mode = "list";
+ MetacatUI.appModel.set("searchMode", "list");
+ gmaps = null;
+ }
+
+ // If this is a subview, don't set the headerType
+ if (!this.isSubView) {
+ MetacatUI.appModel.set("headerType", "default");
+ $("body").addClass("DataCatalog");
+ } else {
+ this.$el.addClass("DataCatalog");
+ }
+ // Populate the search template with some model attributes
+ loadingHTML = this.loadingTemplate({
+ msg: "Loading entries ...",
+ });
- precision = viewRef.mapModel.getSearchPrecision(zoom);
+ 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",
+ };
+ compiledEl = this.template(
+ _.extend(this.searchModel.toJSON(), templateVars)
+ );
+ this.$el.html(compiledEl);
+
+ // Create and render the FilterGroupsView
+ this.createFilterGroups();
+
+ // 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
+ tooltips = $(".tooltip-this");
+
+ // Find the tooltips that are on filter labels - add a slight delay to
+ // those
+ groupedTooltips = _.groupBy(tooltips, function (t) {
+ return (
+ ($(t).prop("tagName") == "LABEL" ||
+ $(t).parent().prop("tagName") == "LABEL") &&
+ $(t).parents(".filter-container").length > 0
+ );
+ });
- // Get all the geohash tiles contained in the map bounds
- if ( south && west && north && east && precision ) {
- geohashBBoxes = nGeohash.bboxes(south, west, north, east, precision);
- }
+ $(groupedTooltips[forFilterLabel]).tooltip({
+ delay: {
+ show: "800",
+ },
+ });
+ $(groupedTooltips[forOtherElements]).tooltip();
- // Save our geohash search settings
- spatialFilter.set({
- "geohashes": geohashBBoxes,
- "geohashLevel": precision,
- "north": north,
- "west": west,
- "south": south,
- "east": east,
- });
+ // Initialize all popover elements
+ $(".popover-this").popover();
- // Add the spatial filter to the filters collection if enabled
- if ( viewRef.searchModel.get("useGeohash") ) {
+ // Initialize the resizeable content div
+ $("#content").resizable({
+ handles: "n,s,e,w",
+ });
- viewRef.searchModel.get("filters").add(spatialFilter);
+ // 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);
+
+ // Listen to changes in the Search model Filters to trigger a search
+ this.stopListening(
+ this.searchModel.get("filters"),
+ "add remove update reset change"
+ );
+ this.listenTo(
+ this.searchModel.get("filters"),
+ "add remove update reset change",
+ this.triggerSearch
+ );
+
+ // 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;
+ },
+
+ /**
+ * Creates UI Filter Groups and renders them in this view. UI Filter
+ * Groups are custom, interactive search filter elements, grouped together
+ * in one panel, section, tab, etc.
+ */
+ createFilterGroups: function () {
+ // If it was already created, then exit
+ if (this.filterGroupsView) {
+ return;
+ }
+
+ // Start an array for the FilterGroups and the individual Filter models
+ var filterGroups = [],
+ allFilters = [];
+
+ // Iterate over each default FilterGroup in the app config and create a
+ // FilterGroup model
+ _.each(
+ MetacatUI.appModel.get("defaultFilterGroups"),
+ function (filterGroupJSON) {
+ // Create the FilterGroup model
+ var filterGroup = new FilterGroup(filterGroupJSON);
+
+ // Add to the array
+ filterGroups.push(filterGroup);
+
+ // Add the Filters to the array
+ allFilters = _.union(allFilters, filterGroup.get("filters").models);
+ },
+ this
+ );
+
+ // Add the filters to the Search model
+ this.searchModel.get("filters").add(allFilters);
+
+ // Create a FilterGroupsView
+ var filterGroupsView = new FilterGroupsView({
+ filterGroups: filterGroups,
+ filters: this.searchModel.get("filters"),
+ vertical: true,
+ parentView: this,
+ editorView: this.editorView,
+ });
- if( viewRef.filterGroupsView && spatialFilter ){
- viewRef.filterGroupsView.addCustomAppliedFilter(spatialFilter);
+ // Add the FilterGroupsView element to this view
+ this.$(this.filterGroupsContainer).html(filterGroupsView.el);
+
+ // Render the FilterGroupsView
+ filterGroupsView.render();
+
+ // Save a reference to the FilterGroupsView
+ this.filterGroupsView = filterGroupsView;
+ },
+
+ /*
+ * Get Results from the Solr index by combining the Filter query string
+ * fragments in each Filter instance in the Search collection and querying
+ * Solr.
+ *
+ * Overrides DataCatalogView.getResults().
+ */
+ getResults: function () {
+ var sortOrder = this.searchModel.get("sortOrder");
+ var query; // The full query string
+ var geohashLevel; // The geohash level to search
+ var page; // The page of search results to render
+ var position; // The geohash level position in the facet array
+
+ // Get the Solr query string from the Search filter collection
+ query = this.searchModel.get("filters").getQuery();
+
+ // If the query hasn't changed since the last query that was sent, don't
+ // do anything. This function may have been triggered by a change event
+ // on a filter that doesn't affect the query at all
+ if (query == this.searchResults.getLastQuery()) {
+ return;
+ }
+
+ if (sortOrder) {
+ this.searchResults.setSort(sortOrder);
+ }
+
+ // Specify which fields to retrieve
+ var fields = [
+ "id",
+ "seriesId",
+ "title",
+ "origin",
+ "pubDate",
+ "dateUploaded",
+ "abstract",
+ "resourceMap",
+ "beginDate",
+ "endDate",
+ "read_count_i",
+ "geohash_9",
+ "datasource",
+ "isPublic",
+ "project",
+ "documents",
+ "label",
+ "logo",
+ "formatId",
+ ];
+ // Add spatial fields if the map is present
+ if (gmaps) {
+ fields.push(
+ "northBoundCoord",
+ "southBoundCoord",
+ "eastBoundCoord",
+ "westBoundCoord"
+ );
+ }
+ // Set the field list on the SolrResults collection as a comma-separated
+ // string
+ this.searchResults.setfields(fields.join(","));
+
+ // Specify which geohash level is used to return tile counts
+ if (gmaps && this.map) {
+ geohashLevel =
+ "geohash_" + this.mapModel.determineGeohashLevel(this.map.zoom);
+ // Does it already exist as a facet field?
+ position = this.searchResults.facet.indexOf(geohashLevel);
+ if (position == -1) {
+ this.searchResults.facet.push(geohashLevel);
+ }
+ }
+
+ // Set the query on the SolrResults collection
+ this.searchResults.setQuery(query);
+
+ // Get the page number
+ if (this.isSubView) {
+ page = 0;
+ } else {
+ page = MetacatUI.appModel.get("page");
+ if (page == null) {
+ page = 0;
+ }
+ }
+ this.searchResults.start = page * this.searchResults.rows;
+
+ // go to the page, which triggers a search
+ this.showPage(page);
+
+ // don't want to follow links
+ return false;
+ },
+
+ /**
+ * Toggle the map filter to include or exclude it from the Solr query
+ */
+ toggleMapFilter: function (event) {
+ 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 (event && event.target.tagName != "INPUT") {
+ isOn = !isOn;
+ toggleInput.prop("checked", isOn);
+ }
+
+ var spatialFilter = _.findWhere(
+ this.searchModel.get("filters").models,
+ { type: "SpatialFilter" }
+ );
+
+ if (isOn) {
+ this.searchModel.set("useGeohash", true);
+
+ if (this.filterGroupsView && spatialFilter) {
+ this.filterGroupsView.addCustomAppliedFilter(spatialFilter);
+ }
+ } else {
+ this.searchModel.set("useGeohash", false);
+ // Remove the spatial filter from the collection
+ this.searchModel.get("filters").remove(spatialFilter);
+
+ if (this.filterGroupsView && spatialFilter) {
+ this.filterGroupsView.removeCustomAppliedFilter(spatialFilter);
+ }
+ }
+
+ // Tell the map to trigger a new search and redraw tiles
+ this.allowSearch = true;
+ google.maps.event.trigger(this.mapModel.get("map"), "idle");
+
+ // Send this event to Google Analytics
+ if (
+ MetacatUI.appModel.get("googleAnalyticsKey") &&
+ typeof ga !== "undefined"
+ ) {
+ var action = isOn ? "on" : "off";
+ ga("send", "event", "map", action);
+ }
+ },
+
+ /**
+ * Overload this function with an empty function since the Clear button
+ * has been moved to the FilterGroupsView
+ */
+ toggleClearButton: function () {},
+
+ /**
+ * Overload this function with an empty function since the Clear button
+ * has been moved to the FilterGroupsView
+ */
+ hideClearButton: function () {},
+
+ /**
+ * Overload this function with an empty function since the Clear button
+ * has been moved to the FilterGroupsView
+ */
+ showClearButton: function () {},
+
+ /**
+ * Toggle between map and list mode
+ *
+ * @param(Event) the event passed by clicking the toggle-map class button
+ */
+ toggleMapMode: function (event) {
+ // Block the event from bubbling
+ if (typeof event === "object") {
+ event.preventDefault();
+ }
+
+ if (gmaps) {
+ $(".mapMode").toggleClass("mapMode");
+ }
+
+ // Toggle the mode
+ 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();
+ }
+ },
+
+ /**
+ * Reset the map to the defaults
+ */
+ resetMap: function () {
+ // The spatial models registered in the filters collection
+ var spatialModels;
+
+ if (!gmaps) {
+ return;
+ }
+
+ // Remove the SpatialFilter from the collection silently so we don't
+ // immediately trigger a new search
+ spatialModels = _.where(this.searchModel.get("filters").models, {
+ type: "SpatialFilter",
+ });
+ this.searchModel.get("filters").remove(spatialModels, { silent: true });
+
+ // Reset the map options to defaults
+ this.mapModel.set("mapOptions", this.mapModel.defaults().mapOptions);
+ this.allowSearch = false;
+ },
+
+ /**
+ * Render the map based on the mapModel properties and search results
+ */
+ 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;
+ }
+
+ // The spatial filter instance used to constrain the search by zoom and
+ // extent
+ var spatialFilter;
+
+ // The map's configuration
+ var mapOptions;
+
+ // The map extent
+ var boundingBox;
+
+ // The map bounding coordinates
+ var north;
+ var west;
+ var south;
+ var east;
+
+ // The map zoom level
+ var zoom;
+
+ // The map geohash precision based on the zoom level
+ var precision;
+
+ // The geohash boxes associated with the map extent and zoom
+ var geohashBBoxes;
+
+ // References to the map and catalog view instances for callbacks
+ var mapRef;
+ var viewRef;
+
+ if (this.isSubView) {
+ this.$el.addClass("mapMode");
+ } else {
+ $("body").addClass("mapMode");
+ }
+
+ // Get the map options and create the map
+ gmaps.visualRefresh = true;
+ mapOptions = this.mapModel.get("mapOptions");
+ var defaultZoom = mapOptions.zoom;
+ $("#map-container").append("
");
+ 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();
+
+ // Get the existing spatial filter if it exists
+ if (
+ this.searchModel.get("filters") &&
+ this.searchModel.get("filters").where({ type: "SpatialFilter" })
+ .length > 0
+ ) {
+ spatialFilter = this.searchModel
+ .get("filters")
+ .where({ type: "SpatialFilter" })[0];
+ } else {
+ spatialFilter = new SpatialFilter();
+ }
+
+ // Store references
+ mapRef = this.map;
+ viewRef = this;
+
+ // Listen to idle events on the map (at rest), and render content as
+ // needed
+ 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) {
+ // Get the Google map bounding box
+ boundingBox = mapRef.getBounds();
+
+ // Set the search model's spatial filter properties Encode the
+ // Google Map bounding box into geohash
+ if (typeof boundingBox !== "undefined") {
+ north = boundingBox.getNorthEast().lat();
+ west = boundingBox.getSouthWest().lng();
+ south = boundingBox.getSouthWest().lat();
+ east = boundingBox.getNorthEast().lng();
+ }
- //When the custom spatial filter is removed in the UI, toggle the map filter
- viewRef.listenTo( viewRef.filterGroupsView, "customAppliedFilterRemoved", function(removedFilter){
+ // Save the center position and zoom level of the map
+ viewRef.mapModel.get("mapOptions").center = mapRef.getCenter();
+ viewRef.mapModel.get("mapOptions").zoom = mapRef.getZoom();
- if( removedFilter.type == "SpatialFilter" ){
+ // Determine the precision of geohashes to search for
+ zoom = mapRef.getZoom();
- //Uncheck the map filter on the map itself
- viewRef.$(".toggle-map-filter").prop("checked", false);
- viewRef.toggleMapFilter();
+ precision = viewRef.mapModel.getSearchPrecision(zoom);
- }
+ // Get all the geohash tiles contained in the map bounds
+ if (south && west && north && east && precision) {
+ geohashBBoxes = nGeohash.bboxes(
+ south,
+ west,
+ north,
+ east,
+ precision
+ );
+ }
- });
- }
+ // Save our geohash search settings
+ spatialFilter.set({
+ geohashes: geohashBBoxes,
+ geohashLevel: precision,
+ north: north,
+ west: west,
+ south: south,
+ east: east,
+ });
+
+ // Add the spatial filter to the filters collection if enabled
+ if (viewRef.searchModel.get("useGeohash")) {
+ viewRef.searchModel.get("filters").add(spatialFilter);
+
+ if (viewRef.filterGroupsView && spatialFilter) {
+ viewRef.filterGroupsView.addCustomAppliedFilter(spatialFilter);
+
+ // When the custom spatial filter is removed in the UI, toggle
+ // the map filter
+ viewRef.listenTo(
+ viewRef.filterGroupsView,
+ "customAppliedFilterRemoved",
+ function (removedFilter) {
+ if (removedFilter.type == "SpatialFilter") {
+ // Uncheck the map filter on the map itself
+ viewRef.$(".toggle-map-filter").prop("checked", false);
+ viewRef.toggleMapFilter();
}
}
- else{
-
- //Reset the map filter
- viewRef.resetMap();
-
- //Start back at page 0
- MetacatUI.appModel.set("page", 0);
+ );
+ }
+ }
+ } else {
+ // Reset the map filter
+ viewRef.resetMap();
- //Mark the view as ready to start a search
- viewRef.ready = true;
+ // Start back at page 0
+ MetacatUI.appModel.set("page", 0);
- // Trigger a new search
- viewRef.triggerSearch();
+ // Mark the view as ready to start a search
+ viewRef.ready = true;
- viewRef.allowSearch = false;
+ // Trigger a new search
+ viewRef.triggerSearch();
- return;
- }
+ viewRef.allowSearch = false;
- });
+ return;
+ }
+ });
- 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, "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, "dragend", function () {
+ viewRef.hasDragged = true;
});
- return DataCatalogViewWithFilters;
- });
+ },
+ }
+ );
+ return DataCatalogViewWithFilters;
+});
diff --git a/src/js/views/filters/BooleanFilterView.js b/src/js/views/filters/BooleanFilterView.js
index 7bf1e7d2b..205b19b89 100644
--- a/src/js/views/filters/BooleanFilterView.js
+++ b/src/js/views/filters/BooleanFilterView.js
@@ -1,79 +1,78 @@
/*global define */
-define(['jquery', 'underscore', 'backbone',
- 'models/filters/BooleanFilter',
- 'views/filters/FilterView',
- 'text!templates/filters/booleanFilter.html'],
- function($, _, Backbone, BooleanFilter, FilterView, Template) {
- 'use strict';
+define([
+ "jquery",
+ "underscore",
+ "backbone",
+ "models/filters/BooleanFilter",
+ "views/filters/FilterView",
+ "text!templates/filters/booleanFilter.html",
+], function ($, _, Backbone, BooleanFilter, FilterView, Template) {
+ "use strict";
/**
- * @class BooleanFilterView
- * @classdesc Render a view of a single BooleanFilter model
- * @classcategory Views/Filters
- */
+ * @class BooleanFilterView
+ * @classdesc Render a view of a single BooleanFilter model
+ * @classcategory Views/Filters
+ */
var BooleanFilterView = FilterView.extend(
- /** @lends BooleanFilterView.prototype */{
-
- /**
- * A BooleanFilter model to be rendered in this view
- * @type {BooleanFilter} */
- model: null,
-
- className: "filter boolean",
-
- template: _.template(Template),
-
- events: {
- "click input[type='checkbox']" : "updateModel"
- },
-
- initialize: function (options) {
-
- if( !options || typeof options != "object" ){
- var options = {};
- }
-
- this.model = options.model || new BooleanFilter();
-
- },
-
- render: function () {
- this.$el.html( this.template( this.model.toJSON() ) );
-
- this.listenTo( this.model, "change:values", this.updateCheckbox );
- },
-
- /**
- * Gets the value of the checkbox and updates the BooleanFilter model
- */
- updateModel: function(){
-
- //Find out if the checkbox has been checked or not
- var isChecked = this.$("input[type='checkbox']").prop("checked");
-
- //Set the boolean value on the model
- this.model.set("values", [isChecked]);
-
- },
-
- /**
- * Updates the checked property of the checkbox based on the model value
- */
- updateCheckbox: function(){
-
- //Get the value from the model
- var modelValue = this.model.get("values")[0];
-
- //If the model value is falsey, then set to false
- if( !modelValue ){
- modelValue = false;
- }
-
- //Update the checkbox based on the model value
- this.$("input[type='checkbox']").prop("checked", modelValue);
-
+ /** @lends BooleanFilterView.prototype */ {
+ /**
+ * A BooleanFilter model to be rendered in this view
+ * @type {BooleanFilter} */
+ model: null,
+
+ className: "filter boolean",
+
+ template: _.template(Template),
+
+ /**
+ * @inheritdoc
+ */
+ events: function(){
+ try {
+ const events = FilterView.prototype.events.call(this);
+ events["click input[type='checkbox']"] = "updateModel";
+ return events
+ }
+ catch (e) {
+ console.log('Failed to create events for BooleanFilterView: ' + e);
+ return {};
+ }
+ },
+
+ render: function (templateVars) {
+ FilterView.prototype.render.call(this, templateVars);
+ this.stopListening(this.model, "change:values");
+ this.listenTo(this.model, "change:values", this.updateCheckbox);
+ },
+
+ /**
+ * Gets the value of the checkbox and updates the BooleanFilter model
+ */
+ updateModel: function () {
+ //Find out if the checkbox has been checked or not
+ var isChecked = this.$("input[type='checkbox']").prop("checked");
+
+ //Set the boolean value on the model
+ this.model.set("values", [isChecked]);
+ },
+
+ /**
+ * Updates the checked property of the checkbox based on the model value
+ */
+ updateCheckbox: function () {
+ //Get the value from the model
+ var modelValue = this.model.get("values")[0];
+
+ //If the model value is falsey, then set to false
+ if (!modelValue) {
+ modelValue = false;
+ }
+
+ //Update the checkbox based on the model value
+ this.$("input[type='checkbox']").prop("checked", modelValue);
+ },
}
-
- });
+ );
return BooleanFilterView;
});
diff --git a/src/js/views/filters/DateFilterView.js b/src/js/views/filters/DateFilterView.js
index e9f3cd3ea..e2e1fc669 100644
--- a/src/js/views/filters/DateFilterView.js
+++ b/src/js/views/filters/DateFilterView.js
@@ -36,7 +36,7 @@ define(['jquery', 'underscore', 'backbone',
try {
var events = FilterView.prototype.events.call(this);
events["change input.max"] = "updateYearRange";
- events["change input.min"] = "updateYearRange"
+ events["change input.min"] = "updateYearRange";
return events
}
catch (error) {
@@ -98,6 +98,15 @@ define(['jquery', 'underscore', 'backbone',
//When the rangeReset event is triggered, reset the slider
this.listenTo(view.model, "rangeReset", this.resetSlider);
+ },
+
+ /**
+ * Override the base view which is triggered when the user types in the
+ * input and presses "Enter". The DateFilterView handles updating the model
+ * already and we do not want to clear the input value at any time.
+ */
+ handleChange: function () {
+ return
},
/**
diff --git a/src/js/views/filters/FilterGroupView.js b/src/js/views/filters/FilterGroupView.js
index 1c9ca7f62..05b9ebccc 100644
--- a/src/js/views/filters/FilterGroupView.js
+++ b/src/js/views/filters/FilterGroupView.js
@@ -47,7 +47,16 @@ define(['jquery', 'underscore', 'backbone',
* @type {boolean}
* @since 2.17.0
*/
- edit: false,
+ edit: false,
+
+ /**
+ * If set to true, then all filters within this group will be collapsible.
+ * See {@link FilterView#collapsible}
+ * @type {boolean}
+ * @since x.x.x
+ * @default false
+ */
+ collapsible: false,
initialize: function (options) {
@@ -65,6 +74,10 @@ define(['jquery', 'underscore', 'backbone',
this.edit = true
}
+ if (options.collapsible && typeof options.collapsible === "boolean") {
+ this.collapsible = options.collapsible;
+ }
+
},
render: function () {
@@ -94,7 +107,8 @@ define(['jquery', 'underscore', 'backbone',
var viewOptions = {
model: filter,
mode: filterMode,
- editorView: this.editorView
+ editorView: this.editorView,
+ collapsible: this.collapsible
}
//Some filters are handled specially
@@ -121,7 +135,7 @@ define(['jquery', 'underscore', 'backbone',
break;
case "BooleanFilter":
// TODO: Set up "edit" and "uiBuilder" mode for BooleanFilters
- var filterView = new BooleanFilterView({ model: filter });
+ var filterView = new BooleanFilterView(viewOptions);
break;
case "ChoiceFilter":
var filterView = new ChoiceFilterView(viewOptions);
diff --git a/src/js/views/filters/FilterGroupsView.js b/src/js/views/filters/FilterGroupsView.js
index b69dfe90e..8e40ed76e 100644
--- a/src/js/views/filters/FilterGroupsView.js
+++ b/src/js/views/filters/FilterGroupsView.js
@@ -69,6 +69,23 @@ define(['jquery', 'underscore', 'backbone',
* @since 2.17.0
*/
edit: false,
+
+ /**
+ * If set to true, then all filters within this group will be collapsible.
+ * See {@link FilterView#collapsible}
+ * @type {boolean}
+ * @since x.x.x
+ * @default false
+ */
+ collapsible: false,
+
+ /**
+ * The initial query to use when the view is first rendered. This is a text value
+ * that will be set on the general `text` Solr field.
+ * @type {string}
+ * @since x.x.x
+ */
+ initialQuery: undefined,
/**
* @inheritdoc
@@ -107,6 +124,14 @@ define(['jquery', 'underscore', 'backbone',
this.edit = true
}
+ if (options.initialQuery) {
+ this.initialQuery = options.initialQuery;
+ }
+
+ if (options.collapsible && typeof options.collapsible === "boolean") {
+ this.collapsible = options.collapsible;
+ }
+
},
/**
@@ -217,7 +242,8 @@ define(['jquery', 'underscore', 'backbone',
var filterGroupView = new FilterGroupView({
model: filterGroup,
edit: this.edit,
- editorView: this.editorView
+ editorView: this.editorView,
+ collapsible: this.collapsible
});
//Render the FilterGroupView
@@ -272,8 +298,9 @@ define(['jquery', 'underscore', 'backbone',
//Render the applied filters
this.renderAppliedFiltersSection();
- //Render an "All" filter
- this.renderAllFilter();
+ // Render an "All" filter. If the view was initialized with an initial
+ // query, set it on this filter.
+ this.renderAllFilter(this.initialQuery);
}
if(this.edit){
@@ -366,7 +393,12 @@ define(['jquery', 'underscore', 'backbone',
},
- renderAllFilter: function(){
+ /**
+ * Renders an "All" filter that will search the general `text` Solr field
+ * @param {string} searchFor - The initial value of the "All" filter. This
+ * will get set on the filter model and trigger a change event. Optional.
+ */
+ renderAllFilter: function (searchFor="") {
//Create an "All" filter that will search the general `text` Solr field
var filter = new Filter({
@@ -387,6 +419,9 @@ define(['jquery', 'underscore', 'backbone',
filterView.render();
this.$(".filters-header").prepend(filterView.el);
+ if (searchFor && searchFor.length) {
+ filter.set('values', [searchFor]);
+ }
},
postRender: function(){
diff --git a/src/js/views/filters/FilterView.js b/src/js/views/filters/FilterView.js
index 5368ee697..3f64d7d59 100644
--- a/src/js/views/filters/FilterView.js
+++ b/src/js/views/filters/FilterView.js
@@ -64,7 +64,41 @@ define(['jquery', 'underscore', 'backbone',
* @type {string}
* @since 2.17.0
*/
- uiBuilderClass: "ui-build",
+ uiBuilderClass: "ui-build",
+
+ /**
+ * Whether the filter is collapsible. If true, the filter will have a button that
+ * toggles the collapsed state.
+ * @type {boolean}
+ * @since x.x.x
+ */
+ collapsible: false,
+
+ /**
+ * The class to add to the filter when it is collapsed.
+ * @type {string}
+ * @since x.x.x
+ * @default "collapsed"
+ */
+ collapsedClass: "collapsed",
+
+ /**
+ * The class used for the button that toggles the collapsed state of the filter.
+ * @type {string}
+ * @since x.x.x
+ * @default "collapse-toggle"
+ */
+ collapseToggleClass: "collapse-toggle",
+
+ /**
+ * The current state of the filter, if it is {@link FilterView#collapsible}.
+ * Whatever this value is set to at initialization, will be how the filter is
+ * initially rendered.
+ * @type {boolean}
+ * @since x.x.x
+ * @default true
+ */
+ collapsed: true,
/**
* The class used for input elements where the user can change UI attributes when this
@@ -86,20 +120,21 @@ define(['jquery', 'underscore', 'backbone',
"click .btn": "handleChange",
"keydown input": "handleTyping"
}
- events["change ." + this.uiInputClass] = "updateUIAttribute"
+ events["change ." + this.uiInputClass] = "updateUIAttribute";
+ events[`click .${this.collapseToggleClass}`] = "toggleCollapse";
return events
}
catch (error) {
console.log( 'There was an error setting the events object in a FilterView' +
' Error details: ' + error );
}
- },
+ },
/**
* Function executed whenever a new FilterView is created.
* @param {Object} [options] - A literal object of options to set on this View
*/
- initialize: function (options) {
+ initialize: function (options) {
try {
if (!options || typeof options != "object") {
@@ -132,6 +167,10 @@ define(['jquery', 'underscore', 'backbone',
this.model = options.model || new this.modelClass();
+ if (options.collapsible && typeof options.collapsible === "boolean") {
+ this.collapsible = options.collapsible;
+ }
+
}
catch (error) {
@@ -155,8 +194,14 @@ define(['jquery', 'underscore', 'backbone',
var templateVars = this.model.toJSON()
}
- // Pass the mode (e.g. "edit", "uiBuilder") to the template
- templateVars = _.extend(templateVars, { mode: this.mode } )
+ // Pass the mode (e.g. "edit", "uiBuilder") to the template, as well
+ // as the variables related to collapsibility.
+ const viewVars = {
+ mode: this.mode,
+ collapsible: this.collapsible,
+ collapseToggleClass: this.collapseToggleClass
+ }
+ templateVars = _.extend(templateVars, viewVars)
// Render the filter HTML (without label or icon)
this.$el.html( this.template( templateVars ) );
@@ -184,6 +229,11 @@ define(['jquery', 'underscore', 'backbone',
if(["edit", "uiBuilder"].includes(this.mode)){
this.$el.find("input").addClass("ignore-changes")
}
+
+ // If the filter is collapsible, set the initial collapsed state
+ if(this.collapsible && typeof this.collapsed === "boolean"){
+ this.toggleCollapse(this.collapsed)
+ }
}
catch (error) {
@@ -371,7 +421,35 @@ define(['jquery', 'underscore', 'backbone',
'. Error details: ' + error
);
}
- }
+ },
+
+ /**
+ * Toggle the collapsed state of the filter. If collapse is a boolean, then set the
+ * collapsed state to that value. Otherwise, set it to the opposite of whichever
+ * state is currently set.
+ * @param {boolean} [collapse] Whether to collapse the filter. If not provided, the
+ * filter will be collapsed if it is currently expanded, and vice versa.
+ * @since x.x.x
+ */
+ toggleCollapse: function (collapse) {
+ try {
+ // If collapse is a boolean, then set the collapsed state to that value.
+ // Otherwise, set it to the opposite of whichever state is currently set.
+ if (typeof collapse !== "boolean") {
+ collapse = !this.collapsed
+ }
+ if (collapse) {
+ this.el.classList.add(this.collapsedClass)
+ this.collapsed = true
+ } else {
+ this.el.classList.remove(this.collapsedClass)
+ this.collapsed = false
+ }
+ }
+ catch (e) {
+ console.log("Could not un/collapse filter.", e);
+ }
+ },
diff --git a/src/js/views/filters/ToggleFilterView.js b/src/js/views/filters/ToggleFilterView.js
index d92a55083..4bd686438 100644
--- a/src/js/views/filters/ToggleFilterView.js
+++ b/src/js/views/filters/ToggleFilterView.js
@@ -37,7 +37,8 @@ define(['jquery', 'underscore', 'backbone',
events: function () {
try {
var events = FilterView.prototype.events.call(this);
- events["change input.toggle-checkbox"] = "updateModel";
+ events["click input[type='checkbox']"] = "updateModel";
+
return events
}
catch (error) {
@@ -49,13 +50,13 @@ define(['jquery', 'underscore', 'backbone',
/**
* @inheritdoc
*/
- render: function () {
+ render: function (templateVars = {}) {
try {
- var templateVars = this.model.toJSON();
+ templateVars = _.extend(this.model.toJSON(), templateVars);
templateVars.id = this.model.cid;
- if( !this.model.get("falseLabel") ){
+ if (!this.model.get("falseLabel")) {
//If the value is the same as the trueValue, the checkbox should be checked
templateVars.checked = (this.model.get("values")[0] == this.model.get("trueValue"))? true : false;
@@ -170,7 +171,7 @@ define(['jquery', 'underscore', 'backbone',
* The filter value is grabbed from the checkbox element in this view.
*
*/
- updateModel: function(){
+ updateModel: function () {
//Check if the checkbox is checked
var isChecked = this.$("input").prop("checked");
diff --git a/src/js/views/maps/CesiumGeohashes.js b/src/js/views/maps/CesiumGeohashes.js
deleted file mode 100644
index 92c9183a5..000000000
--- a/src/js/views/maps/CesiumGeohashes.js
+++ /dev/null
@@ -1,127 +0,0 @@
-define(["backbone",
- "cesium",
- "nGeohash",
- "models/maps/assets/CesiumGeohash"],
- function(Backbone, Cesium, geohash, CesiumGeohash){
-
- /**
- * @class CesiumGeohashes
- * @name CesiumGeohashes
- * @classcategory Views/Maps
- * @extends Backbone.View
- * @classdesc Draws geohash boxes and their associated count/density in a Cesium {@link CesiumWidgetView}. Uses the {@link CesiumGeohash} Map Asset for the geohash data.
- * @since 2.22.0
- */
- return Backbone.View.extend(/** @lends CesiumGeohashes.prototype */{
-
- cesiumViewer: null,
-
- /**
- * A reference to the CesiumGeohash MapAsset model that is rendered in this view
- * @type {CesiumGeohash}
- * @since 2.22.0
- */
- cesiumGeohash: null,
-
- render: function(){
-
- //If there is no CesiumGeohash model, exit without rendering
- if(!this.cesiumGeohash){
- return;
- }
-
- this.entities = this.cesiumGeohash.get("cesiumModel").entities;
-
- if(this.cesiumGeohash.get('status') == "ready"){
- this.drawGeohashes();
- }
-
- //When the status changes, re-render this view
- this.listenTo(this.cesiumGeohash, "change:status", this.drawGeohashes);
-
- this.listenToMovement();
-
- },
-
- /**
- * Listens to Cesium Camera movement so the geohash level can change when the camera zooms in
- * @since 2.22.0
- */
- listenToMovement: function(){
- //Listen to camera movement to change the geohash level
- let view = this;
- this.cesiumViewer.scene.camera.moveEnd.addEventListener(function () {
- //Get the position of the Cesium camera
- let c = Cesium.Cartographic.fromCartesian(new Cesium.Cartesian3(view.cesiumViewer.scene.camera.position.x, view.cesiumViewer.scene.camera.position.y, view.cesiumViewer.scene.camera.position.z))
- //Set the geohash level based on the camera position height
- view.cesiumGeohash.setGeohashLevel(c.height);
- });
- },
-
- /**
- * Draws the geohash polygons on the Cesium map (via {@link CesiumGeohash}) associated with this view. Draws a number representing the
- * count/density associated with that polygon (e.g. number of datasets in that area).
- * @since 2.22.0
- */
- drawGeohashes: function(){
-
- let polygon,
- entities = this.entities,
- dataSource = this.dataSource,
- viewer = this.cesiumViewer,
- hue = this.cesiumGeohash.get("hue");
-
- //If there is no CesiumGeohash model, exit without rendering
- if(!this.cesiumGeohash){
- return;
- }
-
- //Remove all the Entities from the Cesium layer
- entities.removeAll();
-
- let counts = this.cesiumGeohash.get("geohashCounts");
-
- for(let i=0; i < counts.length; i+=2){
-
- let hash = counts[i],
- bbox = geohash.decode_bbox(hash),
- count = counts[i+1],
- alpha = count/this.cesiumGeohash.get("totalCount") + 0.5;
-
- let polygon = entities.add({
- polygon : {
- hierarchy : Cesium.Cartesian3.fromDegreesArray([
- bbox[1], bbox[0],
- bbox[3], bbox[0],
- bbox[3], bbox[2],
- bbox[1], bbox[2] ]),
- height : 1000,
- material : Cesium.Color.fromHsl(hue/360, 0.5, 0.6, alpha),
- outline : true,
- outlineColor : Cesium.Color.WHITE
- },
- show: true
- });
-
- let label = entities.add({
- position : Cesium.Cartesian3.fromDegrees((bbox[3]+bbox[1])/2, (bbox[2]+bbox[0])/2),
- label : {
- text : count.toString(),
- font : '14pt monospace',
- style: Cesium.LabelStyle.FILL,
- outlineWidth : 0,
- scaleByDistance : new Cesium.NearFarScalar(1.5e2, 5.0, 8.0e6, 0.7)
- },
- show: true
- });
-
- }
-
- viewer.dataSourceDisplay.dataSources.add(this.cesiumGeohash.get("cesiumModel"))
-
- }
-
-
- });
-
-});
\ No newline at end of file
diff --git a/src/js/views/maps/CesiumWidgetView.js b/src/js/views/maps/CesiumWidgetView.js
index 3b676e7bf..938036820 100644
--- a/src/js/views/maps/CesiumWidgetView.js
+++ b/src/js/views/maps/CesiumWidgetView.js
@@ -35,6 +35,12 @@ define(
* @screenshot views/maps/CesiumWidgetView.png
* @since 2.18.0
* @constructs
+ * @fires CesiumWidgetView#moved
+ * @fires CesiumWidgetView#moveEnd
+ * @fires CesiumWidgetView#moveStart
+ * @fires Map#moved
+ * @fires Map#moveEnd
+ * @fires Map#moveStart
*/
var CesiumWidgetView = Backbone.View.extend(
/** @lends CesiumWidgetView.prototype */{
@@ -82,20 +88,16 @@ define(
renderFunction: 'add3DTileset'
},
{
- types: ['GeoJsonDataSource'],
+ types: ['GeoJsonDataSource', 'CzmlDataSource'],
renderFunction: 'addVectorData'
},
{
- types: ['BingMapsImageryProvider', 'IonImageryProvider', 'TileMapServiceImageryProvider', 'WebMapTileServiceImageryProvider', 'WebMapServiceImageryProvider'],
+ types: ['BingMapsImageryProvider', 'IonImageryProvider', 'TileMapServiceImageryProvider', 'WebMapTileServiceImageryProvider', 'WebMapServiceImageryProvider', 'OpenStreetMapImageryProvider'],
renderFunction: 'addImagery'
},
{
types: ['CesiumTerrainProvider'],
renderFunction: 'updateTerrain'
- },
- {
- types: ['CesiumGeohash'],
- renderFunction: 'addGeohashes'
}
],
@@ -174,6 +176,7 @@ define(
// We will add a base imagery layer after initialization
imageryProvider: false,
terrain: false,
+ useBrowserRecommendedResolution: false,
// Use explicit rendering to make the widget must faster.
// See https://cesium.com/blog/2018/01/24/cesium-scene-rendering-performance
requestRenderMode: true,
@@ -222,7 +225,7 @@ define(
})
// Go to the home position, if one is set.
- view.flyHome()
+ view.flyHome(0)
// If users are allowed to click on features for more details, initialize
// picking behavior on the map.
@@ -232,6 +235,8 @@ define(
// Set listeners for when the Cesium camera changes a significant amount.
view.camera.changed.addEventListener(function () {
+ view.trigger('moved')
+ view.model.trigger('moved')
// Update the bounding box for the visible area in the Map model
view.updateViewExtent()
// If the scale bar is showing, update the pixel to meter scale on the map
@@ -241,6 +246,15 @@ define(
}
})
+ view.camera.moveEnd.addEventListener(function () {
+ view.trigger('moveEnd')
+ view.model.trigger('moveEnd')
+ })
+ view.camera.moveStart.addEventListener(function () {
+ view.trigger('moveStart')
+ view.model.trigger('moveStart')
+ })
+
// Sets listeners for when the mouse moves, depending on the value of the map
// model's showScaleBar and showFeatureInfo attributes
view.setMouseMoveListeners()
@@ -276,6 +290,8 @@ define(
view.addAsset(terrainModel)
}
+
+
return this
}
@@ -332,6 +348,7 @@ define(
updateDataSourceDisplay: function () {
try {
const view = this;
+ const layers = view.model.get('layers')
var dataSources = view.dataSourceDisplay.dataSources;
if (!dataSources || !dataSources.length) {
@@ -347,7 +364,7 @@ define(
const dataSource = dataSources.get(i);
const visualizers = dataSource._visualizers;
- const assetModel = view.model.get('layers').findWhere({
+ const assetModel = layers.findWhere({
cesiumModel: dataSource
})
const displayReadyBefore = assetModel.get('displayReady')
@@ -427,8 +444,13 @@ define(
// event to update styling of map assets with selected features, and tells the
// parent map view to open the feature details panel.
view.inputHandler.setInputAction(function (movement) {
- var pickedFeature = scene.pick(movement.position);
- view.updateSelectedFeatures([pickedFeature])
+ const pickedFeature = scene.pick(movement.position);
+ const action = view.model.get('clickFeatureAction');
+ if (action === 'showDetails') {
+ view.model.selectFeatures([pickedFeature])
+ } else if (action === 'zoom') {
+ view.flyTo(pickedFeature)
+ }
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
}
@@ -440,94 +462,6 @@ define(
}
},
- /**
- * Given a feature from a vector layer (e.g. a Cesium3DTileFeature), gets any
- * properties that are associated with that feature, the MapAsset model that
- * contains the feature, and the ID that Cesium uses to identify it, and updates
- * the Features collection that is set on the Map's `selectedFeatures` attribute
- * with a new Feature model. NOTE: This currently only works with 3D tile
- * features.
- * @param {Cesium.Cesium3DTileFeature[]} features - An array of Cesium3DTileFeatures to
- * select
- */
- updateSelectedFeatures: function (features) {
-
- try {
- const view = this
- const layers = view.model.get('layers')
-
- // Don't update the selected features collection if the newly selected
- // features are identical
- const oldFeatures = view.model.get('selectedFeatures').getFeatureObjects()
- const noChange = _.isEqual(_.sortBy(features), _.sortBy(oldFeatures))
- if (noChange) {
- return;
- }
-
- // Properties of the selected features to pass to the Map model's
- // selectFeatures function. Passing null will empty the map's selectedFeatures
- // collection
- let featuresAttrs = features ? [] : null
- if (!features || !Array.isArray(features)) {
- features = []
- }
-
- features.forEach(function (feature) {
- if (feature) {
- // To find corresponding MapAsset model in the layers collection
- let cesiumModel = null
- // Attributes to make a new Feature model
- const attrs = {
- properties: {},
- mapAsset: null,
- featureID: null,
- featureObject: feature,
- label: null,
- }
- if (feature instanceof Cesium.Cesium3DTileFeature) {
- // Cesium.Cesium3DTileFeature.primitive gives the Cesium.Cesium3DTileset
- cesiumModel = feature.primitive
- attrs.featureID = feature.pickId ? feature.pickId.key : null
- // Search for a property to use as a label
- attrs.label = feature.getProperty('name') || feature.getProperty('label') || null
- } else {
- // TODO: Test - does feature.id give the entity this work for all datasources ?
- // A picked feature object's ID gives the Cesium.Entity
- attrs.featureObject = feature.id
- // Gives the parent DataSource
- cesiumModel = attrs.featureObject.entityCollection.owner
- attrs.featureID = attrs.featureObject.id
- attrs.label = attrs.featureObject.name
- }
-
- attrs.mapAsset = layers.findWhere({
- cesiumModel: cesiumModel
- })
-
- if (
- attrs.mapAsset &&
- typeof attrs.mapAsset.getPropertiesFromFeature === 'function'
- ) {
- attrs.properties = attrs.mapAsset.getPropertiesFromFeature(attrs.featureObject)
- }
-
- featuresAttrs.push(attrs)
- }
- })
-
- // Pass the new information to the Map's selectFeatures function, which will
- // update the selectFeatures collection set on the Map model
- view.model.selectFeatures(featuresAttrs)
-
- }
- catch (error) {
- console.log(
- 'There was an error updating the selected features collection from a ' +
- 'CesiumWidgetView. Error details: ' + error
- );
- }
- },
-
/**
* Move the camera position and zoom to the specified target entity or position on
* the map, using a nice animation. This function starts the flying/zooming
@@ -578,9 +512,7 @@ define(
try {
const view = this;
- if (typeof options !== 'object') {
- options = {}
- }
+ if (typeof options !== 'object') options = {}
// A target is required
if (!target) {
@@ -616,17 +548,26 @@ define(
// There's no native way of getting the bounding sphere or location from a
// 3DTileFeature!
if (target instanceof Feature) {
- // If the target is a Feature, get the Bounding Sphere for the Feature
- // and call this function again.
- const feature = target.get('featureObject')
- let featureBoundingSphere = new Cesium.BoundingSphere();
+ // If the object saved in the Feature is an Entity, then this
+ // function will get the bounding sphere for the entity on the
+ // next run.
+ setTimeout(() => {
+ view.flyTo(target.get('featureObject'), options)
+ }, 0);
+ return
+ }
+
+ // If the target is a Cesium Entity, then get the bounding sphere for the
+ // entity and call this function again.
+ const entity = target instanceof Cesium.Entity ? target : target.id;
+ if (entity instanceof Cesium.Entity) {
+ let entityBoundingSphere = new Cesium.BoundingSphere();
view.dataSourceDisplay.getBoundingSphere(
- feature, false, featureBoundingSphere
+ entity, false, entityBoundingSphere
)
setTimeout(() => {
- view.flyTo(featureBoundingSphere, options)
+ view.flyTo(entityBoundingSphere, options)
}, 0);
-
return
}
@@ -637,18 +578,16 @@ define(
}
}
- catch (error) {
- console.log(
- 'There was an error navigating to a target position in a CesiumWidgetView' +
- '. Error details: ' + error
- );
+ catch (e) {
+ console.log('Failed to navigate to a target in Cesium.', e);
}
},
/**
* Navigate to the homePosition that's set on the Map.
+ * @param {number} duration The duration of the flight in seconds.
*/
- flyHome: function () {
+ flyHome: function (duration) {
try {
var position = this.model.get('homePosition')
@@ -677,6 +616,9 @@ define(
roll: Cesium.Math.toRadians(position.roll)
}
}
+ if (Cesium.defined(duration)) {
+ target.duration = duration
+ }
this.flyTo(target);
}
@@ -697,18 +639,7 @@ define(
*/
getCameraPosition: function () {
try {
- var camera = this.camera
- var cameraPosition = Cesium.Cartographic.fromCartesian(camera.position)
-
- return {
- longitude: Cesium.Math.toDegrees(cameraPosition.longitude),
- latitude: Cesium.Math.toDegrees(cameraPosition.latitude),
- height: cameraPosition.height,
- heading: Cesium.Math.toDegrees(camera.heading),
- pitch: Cesium.Math.toDegrees(camera.pitch),
- roll: Cesium.Math.toDegrees(camera.roll)
- }
-
+ return this.getDegreesFromCartesian(this.camera.position)
}
catch (error) {
console.log(
@@ -719,98 +650,123 @@ define(
},
/**
- * Update the 'currentViewExtent' attribute in the Map model with the north,
- * south, east, and west-most lat/long that define a bounding box around the
- * currently visible area of the map.
+ * Update the 'currentViewExtent' attribute in the Map model with the
+ * bounding box of the currently visible area of the map.
*/
updateViewExtent: function () {
- try {
- const view = this;
- const camera = view.camera;
- const scene = view.scene;
+ try { this.model.set('currentViewExtent', this.getViewExtent()) }
+ catch (e) { console.log('Failed to update the Map view extent.', e) }
+ },
- // This will be the bounding box of the visible area
- let coords = { north: null, south: null, east: null, west: null }
+ /**
+ * Get the north, south, east, and west-most lat/long that define a
+ * bounding box around the currently visible area of the map. Also gives
+ * the height/ altitude of the camera in meters.
+ * @returns {MapConfig#ViewExtent} The current view extent.
+ */
+ getViewExtent: function () {
+ const view = this;
+ const scene = view.scene;
+ const camera = view.camera;
+ // Get the height in meters
+ const height = camera.positionCartographic.height
- // First try getting the visible bounding box using the simple method
- if (!view.scratchRectangle) {
- // Store the rectangle that we use for the calculation (reduces pressure on
- // garbage collector system since this function is called often).
- view.scratchRectangle = new Cesium.Rectangle();
- }
- var rect = camera.computeViewRectangle(
- scene.globe.ellipsoid, view.scratchRectangle
- );
- coords.north = Cesium.Math.toDegrees(rect.north)
- coords.east = Cesium.Math.toDegrees(rect.east)
- coords.south = Cesium.Math.toDegrees(rect.south)
- coords.west = Cesium.Math.toDegrees(rect.west)
-
- // Check if the resulting coordinates cover the entire globe (happens if some of
- // the sky is visible)
-
- const fullGlobeCoverage = coords.west === -180 && coords.east === 180 &&
- coords.south === -90 && coords.north === 90
-
- // See if we can limit the bounding box to a smaller extent
- if (fullGlobeCoverage) {
-
- // Find points at the top, bottom, right, and left corners of the globe
- const edges = view.findEdges()
-
- // Get the midPoint between the top and bottom points on the globe. Use this
- // to decide if the northern or southern hemisphere is more in view.
- let midPoint = view.findMidpoint(edges.top, edges.bottom)
- if (midPoint) {
-
- // Get the latitude of the mid point
- const midPointLat = view.getDegreesFromCartesian(midPoint).latitude
-
- // Get the latitudes of all the edge points so that we can calculate the
- // southern and northern most coordinate
- const edgeLatitudes = []
- Object.values(edges).forEach(function (point) {
- if (point) {
- edgeLatitudes.push(
- view.getDegreesFromCartesian(point).latitude
- )
- }
- })
+ // This will be the bounding box of the visible area
+ let coords = {
+ north: null, south: null, east: null, west: null, height: height
+ }
- if (midPointLat > 0) {
- // If the midPoint is in the northern hemisphere, limit the southern part
- // of the bounding box to the southern most edge point latitude
- coords.south = Math.min(...edgeLatitudes)
- } else {
- // Vice versa for the southern hemisphere
- coords.north = Math.max(...edgeLatitudes)
+ // First try getting the visible bounding box using the simple method
+ if (!view.scratchRectangle) {
+ // Store the rectangle that we use for the calculation (reduces pressure on
+ // garbage collector system since this function is called often).
+ view.scratchRectangle = new Cesium.Rectangle();
+ }
+ var rect = camera.computeViewRectangle(
+ scene.globe.ellipsoid, view.scratchRectangle
+ );
+ coords.north = Cesium.Math.toDegrees(rect.north)
+ coords.east = Cesium.Math.toDegrees(rect.east)
+ coords.south = Cesium.Math.toDegrees(rect.south)
+ coords.west = Cesium.Math.toDegrees(rect.west)
+
+ // Check if the resulting coordinates cover the entire globe (happens
+ // if some of the sky is visible). If so, limit the bounding box to a
+ // smaller extent
+ if (view.coversGlobe(coords)) {
+
+ // Find points at the top, bottom, right, and left corners of the globe
+ const edges = view.findEdges()
+
+ // Get the midPoint between the top and bottom points on the globe. Use this
+ // to decide if the northern or southern hemisphere is more in view.
+ let midPoint = view.findMidpoint(edges.top, edges.bottom)
+ if (midPoint) {
+
+ // Get the latitude of the mid point
+ const midPointLat = view.getDegreesFromCartesian(midPoint).latitude
+
+ // Get the latitudes of all the edge points so that we can calculate the
+ // southern and northern most coordinate
+ const edgeLatitudes = []
+ Object.values(edges).forEach(function (point) {
+ if (point) {
+ edgeLatitudes.push(
+ view.getDegreesFromCartesian(point).latitude
+ )
}
+ })
+
+ if (midPointLat > 0) {
+ // If the midPoint is in the northern hemisphere, limit the southern part
+ // of the bounding box to the southern most edge point latitude
+ coords.south = Math.min(...edgeLatitudes)
+ } else {
+ // Vice versa for the southern hemisphere
+ coords.north = Math.max(...edgeLatitudes)
}
+ }
- // If not focused directly on one of the poles, then also limit the east and
- // west sides of the bounding box
- const northPointLat = view.getDegreesFromCartesian(edges.top).latitude
- const southPointLat = view.getDegreesFromCartesian(edges.bottom).latitude
+ // If not focused directly on one of the poles, then also limit the east and
+ // west sides of the bounding box
+ const northPointLat = view.getDegreesFromCartesian(edges.top).latitude
+ const southPointLat = view.getDegreesFromCartesian(edges.bottom).latitude
- if (northPointLat > 25 && southPointLat < -25) {
- if (edges.right) {
- coords.east = view.getDegreesFromCartesian(edges.right).longitude
- }
- if (edges.left) {
- coords.west = view.getDegreesFromCartesian(edges.left).longitude
- }
+ if (northPointLat > 25 && southPointLat < -25) {
+ if (edges.right) {
+ coords.east = view.getDegreesFromCartesian(edges.right).longitude
+ }
+ if (edges.left) {
+ coords.west = view.getDegreesFromCartesian(edges.left).longitude
}
}
+ }
- view.model.set('currentViewExtent', coords)
+ return coords
+ },
- }
- catch (error) {
- console.log(
- 'Failed to update the Map view extent from a CesiumWidgetView' +
- '. Error details: ' + error
- );
- }
+ /**
+ * Check if a given bounding box covers the entire globe.
+ * @param {Object} coords - An object with the north, south, east, and
+ * west coordinates of a bounding box
+ * @param {Number} latAllowance - The number of degrees latitude to
+ * allow as a buffer. If the north and south coords range from -90 to
+ * 90, minus this buffer * 2, then it is considered to cover the globe.
+ * @param {Number} lonAllowance - The number of degrees longitude to
+ * allow as a buffer.
+ * @returns {Boolean} Returns true if the bounding box covers the entire
+ * globe, false otherwise.
+ */
+ coversGlobe: function (coords, latAllowance = 0.5, lonAllowance = 1) {
+ const maxLat = 90 - latAllowance;
+ const minLat = -90 + latAllowance;
+ const maxLon = 180 - lonAllowance;
+ const minLon = -180 + lonAllowance;
+
+ return coords.west <= minLon &&
+ coords.east >= maxLon &&
+ coords.south <= minLat &&
+ coords.north >= maxLat
},
/**
@@ -821,11 +777,16 @@ define(
*/
getDegreesFromCartesian: function (cartesian) {
const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
- return {
- longitude: Cesium.Math.toDegrees(cartographic.longitude),
- latitude: Cesium.Math.toDegrees(cartographic.latitude),
+ const degrees = {
height: cartographic.height
}
+ const coordinates = ['longitude', 'latitude', 'heading', 'pitch', 'roll']
+ coordinates.forEach(function (coordinate) {
+ if (Cesium.defined(cartographic[coordinate])) {
+ degrees[coordinate] = Cesium.Math.toDegrees(cartographic[coordinate])
+ }
+ });
+ return degrees
},
/**
@@ -1005,13 +966,7 @@ define(
var pickRay = view.camera.getPickRay(mousePosition);
var cartesian = view.scene.globe.pick(pickRay, view.scene);
if (cartesian) {
- // Use globe.ellipsoid.cartesianToCartographic ?
- var cartographic = Cesium.Cartographic.fromCartesian(cartesian);
- view.model.set('currentPosition', {
- latitude: Cesium.Math.toDegrees(cartographic.latitude),
- longitude: Cesium.Math.toDegrees(cartographic.longitude),
- height: cartographic.height,
- })
+ view.model.set('currentPosition', view.getDegreesFromCartesian(cartesian))
}
}
@@ -1139,7 +1094,7 @@ define(
* @param {MapAsset} mapAsset A MapAsset layer to render in the map, such as a
* Cesium3DTileset or a CesiumImagery model.
*/
- addAsset: function(mapAsset) {
+ addAsset: function (mapAsset) {
try {
if (!mapAsset) {
return
@@ -1222,29 +1177,10 @@ define(
this.dataSourceCollection.add(cesiumModel)
},
- /**
- * Renders a CesiumGeohash map asset on the map
- * */
- addGeohashes: function () {
- let view = this;
-
- require(["views/maps/CesiumGeohashes"], (CesiumGeohashes)=>{
- //Create a CesiumGeohashes view
- let cg = new CesiumGeohashes();
- cg.cesiumViewer = view;
-
- //Get the CesiumGeohash MapAsset and save a reference in the view
- let cesiumGeohashAsset = view.model.get('layers').find(mapAsset => mapAsset.get("type") == "CesiumGeohash");
- cg.cesiumGeohash = cesiumGeohashAsset;
-
- cg.render();
- })
- },
-
/**
* Renders imagery in the Map.
* @param {Cesium.ImageryLayer} cesiumModel The Cesium imagery model to render
- */
+ */
addImagery: function (cesiumModel) {
this.scene.imageryLayers.add(cesiumModel)
this.sortImagery()
diff --git a/src/js/views/maps/LayerListView.js b/src/js/views/maps/LayerListView.js
index 6705e93b1..e55ce24ee 100644
--- a/src/js/views/maps/LayerListView.js
+++ b/src/js/views/maps/LayerListView.js
@@ -6,7 +6,6 @@ define(
'jquery',
'underscore',
'backbone',
- 'collections/maps/MapAssets',
'text!templates/maps/layer-list.html',
// Sub-views
'views/maps/LayerItemView'
@@ -15,7 +14,6 @@ define(
$,
_,
Backbone,
- MapAssets,
Template,
// Sub-views
LayerItemView
diff --git a/src/js/views/maps/LegendView.js b/src/js/views/maps/LegendView.js
index ab8f32b15..80b60f32b 100644
--- a/src/js/views/maps/LegendView.js
+++ b/src/js/views/maps/LegendView.js
@@ -139,6 +139,10 @@ define(
try {
+ if (!this.model) {
+ return;
+ }
+
// Save a reference to this view
var view = this;
diff --git a/src/js/views/maps/MapView.js b/src/js/views/maps/MapView.js
index b7147ac9e..d26fa5085 100644
--- a/src/js/views/maps/MapView.js
+++ b/src/js/views/maps/MapView.js
@@ -162,7 +162,10 @@ define(
if (this.model.get('showScaleBar')) {
this.renderScaleBar();
}
- if (this.model.get('showFeatureInfo')) {
+ if (
+ this.model.get('showFeatureInfo') &
+ this.model.get('clickFeatureAction') === 'showDetails'
+ ) {
this.renderFeatureInfo();
}
diff --git a/src/js/views/maps/ToolbarView.js b/src/js/views/maps/ToolbarView.js
index 7df0627b4..ad50f2da7 100644
--- a/src/js/views/maps/ToolbarView.js
+++ b/src/js/views/maps/ToolbarView.js
@@ -7,6 +7,7 @@ define(
'underscore',
'backbone',
'text!templates/maps/toolbar.html',
+ 'models/maps/Map',
// Sub-views
'views/maps/LayerListView'
],
@@ -15,6 +16,7 @@ define(
_,
Backbone,
Template,
+ Map,
// Sub-views
LayerListView
) {
@@ -193,9 +195,22 @@ define(
this[key] = value;
}
}
- if(this.model && this.model.get('toolbarOpen') === true) {
+ if (!this.model || !(this.model instanceof Map)) {
+ this.model = new Map();
+ }
+ if(this.model.get('toolbarOpen') === true) {
this.isOpen = true;
}
+ if (this.model.get("showLayerList") === false) {
+ this.sections = this.sections.filter(
+ (section) => section.label !== "Layers"
+ );
+ }
+ if (this.model.get("showHomeButton") === false) {
+ this.sections = this.sections.filter(
+ (section) => section.label !== "Home"
+ );
+ }
} catch (e) {
console.log('A ToolbarView failed to initialize. Error message: ' + e);
}
diff --git a/src/js/views/search/CatalogSearchView.js b/src/js/views/search/CatalogSearchView.js
index a21d5c0a9..5f23aa7c9 100644
--- a/src/js/views/search/CatalogSearchView.js
+++ b/src/js/views/search/CatalogSearchView.js
@@ -1,508 +1,849 @@
/*global define */
-define(["jquery",
- "backbone",
- "collections/maps/MapAssets",
- "models/filters/FilterGroup",
- "models/connectors/Filters-Search",
- "models/connectors/Geohash-Search",
- "models/maps/assets/CesiumGeohash",
- "models/maps/Map",
- "views/search/SearchResultsView",
- "views/filters/FilterGroupsView",
- "views/maps/MapView",
- "views/search/SearchResultsPagerView",
- "views/search/SorterView"
- ],
-function($, Backbone, MapAssets, FilterGroup, FiltersSearchConnector, GeohashSearchConnector, CesiumGeohash, Map, SearchResultsView, FilterGroupsView, MapView, PagerView, SorterView){
-
- "use strict";
-
- /**
- * @class CatalogSearchView
- * @name CatalogSearchView
- * @classcategory Views
- * @extends Backbone.View
- * @constructor
- * @since 2.22.0
- */
- return Backbone.View.extend(
- /** @lends CatalogSearchView.prototype */ {
-
- /**
- * The type of View this is
- * @type {string}
- */
- type: "CatalogSearch",
-
- /**
- * The HTML tag to use for this view's element
- * @type {string}
- */
- tagName: "section",
-
- /**
- * The HTML classes to use for this view's element
- * @type {string}
- */
- className: "catalog-search-view",
-
- template: `
-
- `,
-
- /**
- * The search mode to use. This can be set to either `map` or `list`. List mode will hide all map features.
- * @type string
- * @since 2.22.0
- * @default "map"
- */
- mode: "map",
-
- searchResults: null,
-
- filterGroupsView: null,
-
- /**
- * @type {PagerView}
- */
- pagerView: null,
-
- /**
- * @type {SorterView}
- */
- sorterView: null,
-
- searchModel: null,
-
- allFilters: null,
-
- filterGroups: null,
-
- /**
- * An array of literal objects to transform into FilterGroup models. These FilterGroups will be displayed in this view and used for searching. If not provided, the {@link AppConfig#defaultFilterGroups} will be used.
- * @type {FilterGroup#defaults[]}
- */
- filterGroupsJSON: null,
-
-
- /**
- * The jQuery selector for the FilterGroupsView container
- * @type {string}
- */
- filterGroupsContainer: ".filter-groups-container",
-
- /**
- * The query selector for the SearchResultsView container
- * @type {string}
- */
- searchResultsContainer: ".search-results-container",
-
- /**
- * The query selector for the CesiumWidgetView container
- * @type {string}
- */
- mapContainer: ".map-container",
-
- /**
- * The query selector for the PagerView container
- * @type {string}
- */
- pagerContainer: ".pager-container",
-
- /**
- * The query selector for the SorterView container
- * @type {string}
- */
- sorterContainer: ".sorter-container",
-
- /**
- * The query selector for the title container
- * @type {string}
- */
- titleContainer: ".title-container",
-
- /**
- * The events this view will listen to and the associated function to call.
- * @type {Object}
- */
- events: {
- "click .map-toggle-container" : "toggleMode"
- },
-
- render: function(){
-
- //Set the search mode - either map or list
- this.setMode();
-
- //Set up the view for styling and layout
+define([
+ "jquery",
+ "backbone",
+ "views/search/SearchResultsView",
+ "views/filters/FilterGroupsView",
+ "views/maps/MapView",
+ "views/search/SearchResultsPagerView",
+ "views/search/SorterView",
+ "text!templates/search/catalogSearch.html",
+ "models/connectors/Map-Search-Filters",
+ "text!" + MetacatUI.root + "/css/catalog-search-view.css",
+], function (
+ $,
+ Backbone,
+ SearchResultsView,
+ FilterGroupsView,
+ MapView,
+ PagerView,
+ SorterView,
+ Template,
+ MapSearchFiltersConnector,
+ CatalogSearchViewCSS
+) {
+ "use strict";
+
+ /**
+ * @class CatalogSearchView
+ * @classdesc The data catalog search view for the repository. This view
+ * displays a Cesium map, search results, and search filters.
+ * @name CatalogSearchView
+ * @classcategory Views
+ * @extends Backbone.View
+ * @constructor
+ * @since 2.22.0
+ * @screenshot views/search/CatalogSearchView.png
+ */
+ return Backbone.View.extend(
+ /** @lends CatalogSearchView.prototype */ {
+ /**
+ * The type of View this is
+ * @type {string}
+ * @since 2.22.0
+ */
+ type: "CatalogSearch",
+
+ /**
+ * The HTML tag to use for this view's element
+ * @type {string}
+ * @since 2.22.0
+ */
+ tagName: "section",
+
+ /**
+ * The HTML classes to use for this view's element
+ * @type {string}
+ * @since 2.22.0
+ */
+ className: "catalog",
+
+ /**
+ * The template to use for this view's element
+ * @type {underscore.template}
+ * @since 2.22.0
+ */
+ template: _.template(Template),
+
+ /**
+ * The template to use in case there is a major error in rendering the
+ * view.
+ * @type {string}
+ * @since x.x.x
+ */
+ errorTemplate: `
+
There was an error loading the search results.
+
Please try again later.
+
`,
+
+ /**
+ * Whether the map is displayed or hidden.
+ * @type boolean
+ * @since x.x.x
+ * @default true
+ */
+ mapVisible: true,
+
+ /**
+ * Whether the filters are displayed or hidden.
+ * @type boolean
+ * @since x.x.x
+ * @default true
+ */
+ filtersVisible: true,
+
+ /**
+ * Whether to limit the search to the extent of the map. If true, the
+ * search will update when the user pans or zooms the map.
+ * @type {boolean}
+ * @since x.x.x
+ * @default true
+ */
+ limitSearchToMapArea: true,
+
+ /**
+ * The View that displays the search results. The render method will be
+ * attach the search results view to the
+ * {@link CatalogSearchView#searchResultsContainer} element and will add
+ * the view reference to this property.
+ * @type {SearchResultsView}
+ * @since 2.22.0
+ */
+ searchResultsView: null,
+
+ /**
+ * The view that shows the search filters. The render method will attach
+ * the filter groups view to the
+ * {@link CatalogSearchView#filterGroupsContainer} element and will add
+ * the view reference to this property.
+ * @type {FilterGroupsView}
+ * @since 2.22.0
+ */
+ filterGroupsView: null,
+
+ /**
+ * The view that shows the number of pages and allows the user to navigate
+ * between them. The render method will attach the pager view to the
+ * {@link CatalogSearchView#pagerContainer} element and will add the view
+ * reference to this property.
+ * @type {PagerView}
+ * @since 2.22.0
+ */
+ pagerView: null,
+
+ /**
+ * The view that handles sorting the search results. The render method
+ * will attach the sorter view to the
+ * {@link CatalogSearchView#sorterContainer} element and will add the view
+ * reference to this property.
+ * @type {SorterView}
+ * @since 2.22.0
+ */
+ sorterView: null,
+
+ /**
+ * The CSS class to add to the body element when this view is rendered.
+ * @type {string}
+ * @since 2.22.0
+ * @default "catalog-search-body",
+ */
+ bodyClass: "catalog-search-body",
+
+ /**
+ * The jQuery selector for the FilterGroupsView container
+ * @type {string}
+ * @since 2.22.0
+ */
+ filterGroupsContainer: ".catalog__filters",
+
+ /**
+ * The query selector for the SearchResultsView container
+ * @type {string}
+ * @since 2.22.0
+ */
+ searchResultsContainer: ".catalog__results-list",
+
+ /**
+ * The query selector for the CesiumWidgetView container
+ * @type {string}
+ * @since 2.22.0
+ */
+ mapContainer: ".catalog__map",
+
+ /**
+ * The query selector for the PagerView container
+ * @type {string}
+ * @since 2.22.0
+ */
+ pagerContainer: ".catalog__pager",
+
+ /**
+ * The query selector for the SorterView container
+ * @type {string}
+ * @since 2.22.0
+ */
+ sorterContainer: ".catalog__sorter",
+
+ /**
+ * The query selector for the title container
+ * @type {string}
+ * @since 2.22.0
+ */
+ titleContainer: ".catalog__summary",
+
+ /**
+ * The query selector for button that is used to either show or hide the
+ * map.
+ * @type {string}
+ * @since 2.22.0
+ */
+ toggleMapButton: ".catalog__map-toggle",
+
+ /**
+ * The query selector for the label that is used to describe the
+ * {@link CatalogSearchView#toggleMapButton}.
+ * @type {string}
+ * @since x.x.x
+ * @default "#toggle-map-label"
+ */
+ toggleMapLabel: "#toggle-map-label",
+
+ /**
+ * The query selector for the button that is used to either show or hide
+ * the filters.
+ * @type {string}
+ * @since x.x.x
+ */
+ toggleFiltersButton: ".catalog__filters-toggle",
+
+ /**
+ * The query selector for the label that is used to describe the
+ * {@link CatalogSearchView#toggleFiltersButton}.
+ * @type {string}
+ * @since x.x.x
+ * @default "#toggle-map-label"
+ */
+ toggleFiltersLabel: "#toggle-filters-label",
+
+ /**
+ * The query selector for the button that is used to turn on or off
+ * spatial filtering by map extent.
+ * @type {string}
+ * @since x.x.x
+ */
+ mapFilterToggle: ".catalog__map-filter-toggle",
+
+ /**
+ * The CSS class (not selector) to add to the body element when the map is
+ * hidden.
+ * @type {string}
+ * @since x.x.x
+ */
+ hideMapClass: "catalog--map-hidden",
+
+ /**
+ * The CSS class (not selector) to add to the body element when the
+ * filters are hidden.
+ * @type {string}
+ * @since x.x.x
+ */
+ hideFiltersClass: "catalog--filters-hidden",
+
+ /**
+ * The events this view will listen to and the associated function to
+ * call.
+ * @type {Object}
+ * @since 2.22.0
+ */
+ events: function () {
+ const e = {};
+ e[`click ${this.mapFilterToggle}`] = "toggleMapFilter";
+ e[`click ${this.toggleMapButton}`] = "toggleMapVisibility";
+ e[`click ${this.toggleFiltersButton}`] = "toggleFiltersVisibility";
+ return e;
+ },
+
+ /**
+ * Initialize the view. In addition to the options described below, any
+ * option that is available in the
+ * {@link MapSearchFiltersConnector#initialize} method can be passed to
+ * this view, such as Map, SolrResult, and FilterGroup models, and whether
+ * to create a geohash layer or spatial filter if they are not present.
+ * @param {Object} options - The options for this view.
+ * @param {string} [options.initialQuery] - The initial text query to run
+ * when the view is rendered.
+ * @param {MapSearchFiltersConnector} [options.model] - A
+ * MapSearchFiltersConnector model to use for this view. If not provided,
+ * a new one will be created. If one is provided, then other options that
+ * would be passed to the MapSearchFiltersConnector model will be ignored
+ * (such as map, searchResults, filterGroups, catalogSearch, etc.)
+ * @since x.x.x
+ */
+ initialize: function (options) {
+ this.cssID = "catalogSearchView";
+ MetacatUI.appModel.addCSS(CatalogSearchViewCSS, this.cssID);
+
+ if (!options) options = {};
+
+ this.initialQuery = options.initialQuery || null;
+
+ let model = options.model;
+ if (!model) {
+ const app = MetacatUI.appModel;
+ model = new MapSearchFiltersConnector({
+ map: options.map || app.get("catalogSearchMapOptions"),
+ searchResults: options.searchResults || null,
+ filterGroups:
+ options.filterGroups || app.get("defaultFilterGroups"),
+ catalogSearch: options.catalogSearch !== false,
+ addGeohashLayer: options.addGeohashLayer !== false,
+ addSpatialFilter: options.addSpatialFilter !== false,
+ });
+ }
+ model.connect();
+ this.model = model;
+ },
+
+ /**
+ * Renders the view
+ * @since 2.22.0
+ */
+ render: function () {
+ // Set the search mode - either map or list
+ this.setMapVisibility();
+
+ // Set up the view for styling and layout
this.setupView();
- //Set up the search and search result models
- this.setupSearch();
-
- //Render the search components
+ // Render the search components
this.renderComponents();
-
- },
-
- /**
- * Sets up the basic components of this view
- */
- setupView: function(){
- document.querySelector("body").classList.add(`catalog-search-body`, `${this.mode}Mode`);
-
- //Add LinkedData to the page
- this.addLinkedData();
-
- this.$el.html(this.template);
- },
-
- /**
- * Sets the search mode (map or list)
- * @since 2.22.0
- */
- setMode: function(){
- //Get the search mode - either "map" or "list"
- if (((typeof this.mode === "undefined") || !this.mode) && (MetacatUI.appModel.get("enableCesium"))) {
- this.mode = "map";
+ },
+
+ /**
+ * Indicates that there was a problem rendering this view.
+ * @since x.x.x
+ */
+ renderError: function () {
+ this.$el.html(this.errorTemplate);
+ },
+
+ /**
+ * Sets the search mode (map or list)
+ * @since 2.22.0
+ */
+ setMapVisibility: function () {
+ try {
+ if (
+ typeof this.mapVisible === "undefined" &&
+ MetacatUI.appModel.get("enableCesium")
+ ) {
+ this.mapVisible = true;
+ }
+
+ // Use map mode on tablets and browsers only. TODO: should we set a
+ // listener for window resize?
+ if ($(window).outerWidth() <= 600) {
+ this.mapVisible = false;
+ }
+ } catch (e) {
+ console.error(
+ "Error setting the search mode, defaulting to list:" + e
+ );
+ this.mapVisible = false;
}
-
- // Use map mode on tablets and browsers only
- if ($(window).outerWidth() <= 600) {
- this.mode = "list";
+ this.toggleMapVisibility(this.mapVisible);
+ },
+
+ /**
+ * Sets up the basic components of this view
+ * @since 2.22.0
+ */
+ setupView: function () {
+ try {
+ // The body class modifies the entire page layout to accommodate the
+ // catalog search view
+ if (!this.isSubView) {
+ MetacatUI.appModel.set("headerType", "default");
+ document.querySelector("body").classList.add(this.bodyClass);
+ } else {
+ // TODO: Set up styling for sub-view version of the catalog
+ }
+
+ // Add LinkedData to the page
+ this.addLinkedData();
+
+ // Render the template
+ this.$el.html(this.template({}));
+ } catch (e) {
+ console.log(
+ "There was an error setting up the CatalogSearchView:" + e
+ );
+ this.renderError();
}
- },
-
- renderComponents: function(){
- this.renderFilters();
-
- //Render the list of search results
- this.renderSearchResults();
-
- //Render the Title
- this.renderTitle();
- this.listenTo(this.searchResultsView.searchResults, "reset", this.renderTitle);
-
- //Render Pager
- this.renderPager();
-
- //Render Sorter
- this.renderSorter();
-
- //Render Cesium
- this.renderMap();
- },
-
- /**
- * Renders the search filters
- * @since 2.22.0
- */
- renderFilters: function(){
- //Render FilterGroups
- this.filterGroupsView = new FilterGroupsView({
- filterGroups: this.filterGroups,
- filters: this.connector?.get("filters"),
+ },
+
+ /**
+ * Calls other methods that insert the sub-views into the DOM and render
+ * them.
+ * @since 2.22.0
+ */
+ renderComponents: function () {
+ try {
+ this.createSearchResults();
+
+ this.createMap();
+
+ this.renderFilters();
+
+ // Render the list of search results
+ this.renderSearchResults();
+
+ // Render the Title
+ this.renderTitle();
+ this.listenTo(
+ this.model.get("searchResults"),
+ "reset",
+ this.renderTitle
+ );
+
+ // Render Pager
+ this.renderPager();
+
+ // Render Sorter
+ this.renderSorter();
+
+ // Render Cesium
+ this.renderMap();
+ } catch (e) {
+ console.log(
+ "There was an error rendering the CatalogSearchView:" + e
+ );
+ this.renderError();
+ }
+ },
+
+ /**
+ * Renders the search filters
+ * @since 2.22.0
+ */
+ renderFilters: function () {
+ try {
+ // Render FilterGroups
+ this.filterGroupsView = new FilterGroupsView({
+ filterGroups: this.model.get("filterGroups"),
+ filters: this.model.get("filters"),
vertical: true,
- parentView: this
- });
-
- //Add the FilterGroupsView element to this view
- this.$(this.filterGroupsContainer).html(this.filterGroupsView.el);
-
- //Render the FilterGroupsView
- this.filterGroupsView.render();
- },
-
- /**
- * Creates the SearchResultsView and saves a reference to the SolrResults collection
- * @since 2.22.0
- */
- createSearchResults: function(){
- this.searchResultsView = new SearchResultsView();
-
- if( this.connector ){
- this.searchResultsView.searchResults = this.connector.get("searchResults");
+ parentView: this,
+ initialQuery: this.initialQuery,
+ collapsible: true,
+ });
+
+ // Add the FilterGroupsView element to this view
+ this.$(this.filterGroupsContainer).html(this.filterGroupsView.el);
+
+ // Render the FilterGroupsView
+ this.filterGroupsView.render();
+ } catch (e) {
+ console.log("There was an error rendering the FilterGroupsView:" + e);
}
- },
-
- /**
- * Renders the search result list
- * @since 2.22.0
- */
- renderSearchResults: function(){
- if(!this.searchResultsView) return;
-
- //Add the view element to this view
- this.$(this.searchResultsContainer).html(this.searchResultsView.el);
-
- //Render the view
- this.searchResultsView.render();
- },
-
- /**
- * Creates a PagerView and adds it to the page.
- * @since 2.22.0
- */
- renderPager: function(){
- this.pagerView = new PagerView();
-
- //Give the PagerView the SearchResults to listen to and update
- this.pagerView.searchResults = this.searchResultsView.searchResults;
-
- //Add the pager view to the page
- this.el.querySelector(this.pagerContainer).replaceChildren(this.pagerView.el);
-
- //Render the pager view
- this.pagerView.render();
- },
-
- /**
- * Creates a SorterView and adds it to the page.
- * @since 2.22.0
- */
- renderSorter: function(){
- this.sorterView = new SorterView();
-
- //Give the SorterView the SearchResults to listen to and update
- this.sorterView.searchResults = this.searchResultsView.searchResults;
-
- //Add the sorter view to the page
- this.el.querySelector(this.sorterContainer).replaceChildren(this.sorterView.el);
-
- //Render the sorter view
- this.sorterView.render();
- },
-
- /**
- * Constructs an HTML string of the title of this view
- * @param {number} start
- * @param {number} end
- * @param {number} numFound
- * @returns {string}
- * @since 2.22.0
- */
- titleTemplate: function(start, end, numFound){
- let html = `
`;
- return html;
- },
-
- /**
- * Updates the view title using the {@link CatalogSearchView#searchResults} data.
- * @since 2.22.0
- */
- renderTitle: function(){
- let titleEl = this.el.querySelector(this.titleContainer);
-
- if(!titleEl){
+ },
+
+ /**
+ * Renders the search result list
+ * @since 2.22.0
+ */
+ renderSearchResults: function () {
+ try {
+ if (!this.searchResultsView) return;
+
+ // Add the view element to this view
+ this.$(this.searchResultsContainer).html(this.searchResultsView.el);
+
+ // Render the view
+ this.searchResultsView.render();
+ } catch (e) {
+ console.log(
+ "There was an error rendering the SearchResultsView:" + e
+ );
+ }
+ },
+
+ /**
+ * Creates a PagerView and adds it to the page.
+ * @since 2.22.0
+ */
+ renderPager: function () {
+ try {
+ this.pagerView = new PagerView();
+
+ // Give the PagerView the SearchResults to listen to and update
+ this.pagerView.searchResults = this.model.get("searchResults");
+
+ // Add the pager view to the page
+ this.el
+ .querySelector(this.pagerContainer)
+ .replaceChildren(this.pagerView.el);
+
+ // Render the pager view
+ this.pagerView.render();
+ } catch (e) {
+ console.log("There was an error rendering the PagerView:" + e);
+ }
+ },
+
+ /**
+ * Creates a SorterView and adds it to the page.
+ * @since 2.22.0
+ */
+ renderSorter: function () {
+ try {
+ this.sorterView = new SorterView();
+
+ // Give the SorterView the SearchResults to listen to and update
+ this.sorterView.searchResults = this.model.get("searchResults");
+
+ // Add the sorter view to the page
+ this.el
+ .querySelector(this.sorterContainer)
+ .replaceChildren(this.sorterView.el);
+
+ // Render the sorter view
+ this.sorterView.render();
+ } catch (e) {
+ console.log("There was an error rendering the SorterView:" + e);
+ }
+ },
+
+ /**
+ * Constructs an HTML string of the title of this view
+ * @param {number} start
+ * @param {number} end
+ * @param {number} numFound
+ * @returns {string}
+ * @since 2.22.0
+ */
+ titleTemplate: function (start, end, numFound) {
+ try {
+ let content = "";
+ const csn = MetacatUI.appView.commaSeparateNumber;
+ if (numFound < end) end = numFound;
+
+ if (numFound > 0) {
+ content = `
${csn(start)} to
${csn(end)} `;
+ if (typeof numFound == "number") {
+ content += ` of
${csn(numFound)} `;
+ }
+ }
+ return `
+
+
+
`;
+ } catch (e) {
+ console.log("There was an error creating the title template:" + e);
+ return "";
+ }
+ },
+
+ /**
+ * Updates the view title using the
+ * {@link CatalogSearchView#searchResults} data.
+ * @since 2.22.0
+ */
+ renderTitle: function () {
+ try {
+ const searchResults = this.model.get("searchResults");
+ let titleEl = this.el.querySelector(this.titleContainer);
+
+ if (!titleEl) {
titleEl = document.createElement("div");
titleEl.classList.add("title-container");
this.el.prepend(titleEl);
- }
-
- titleEl.innerHTML="";
-
- let title = this.titleTemplate(this.searchResultsView.searchResults.getStart()+1, this.searchResultsView.searchResults.getEnd()+1, this.searchResultsView.searchResults.getNumFound());
-
- titleEl.insertAdjacentHTML("beforeend", title);
-
- },
-
- /**
- * Creates the Filter models and SolrResults that will be used for searches
- * @since 2.22.0
- */
- setupSearch: function(){
-
- //Get an array of all Filter models
- let allFilters = [];
- this.filterGroups = this.createFilterGroups();
- this.filterGroups.forEach(group => {
- allFilters = allFilters.concat(group.get("filters")?.models);
- });
-
- //Connect the filters to the search and search results
- let connector = new FiltersSearchConnector({ filtersList: allFilters });
- this.connector = connector;
- connector.startListening();
+ }
- this.createSearchResults();
+ titleEl.innerHTML = "";
- this.createMap();
- },
+ let title = this.titleTemplate(
+ searchResults.getStart() + 1,
+ searchResults.getEnd() + 1,
+ searchResults.getNumFound()
+ );
- /**
- * Creates UI Filter Groups. UI Filter Groups
- * are custom, interactive search filter elements, grouped together in one
- * panel, section, tab, etc.
- * @param {FilterGroup#defaults[]} filterGroupsJSON An array of literal objects to transform into FilterGroup models. These FilterGroups will be displayed in this view and used for searching. If not provided, the {@link AppConfig#defaultFilterGroups} will be used.
- * @since 2.22.0
- */
- createFilterGroups: function(filterGroupsJSON=this.filterGroupsJSON){
-
- try{
- //Start an array for the FilterGroups and the individual Filter models
- let filterGroups = [];
-
- //Iterate over each default FilterGroup in the app config and create a FilterGroup model
- (filterGroupsJSON || MetacatUI.appModel.get("defaultFilterGroups")).forEach( filterGroupJSON => {
-
- //Create the FilterGroup model
- //Add to the array
- filterGroups.push(new FilterGroup(filterGroupJSON));
-
- });
-
- return filterGroups;
+ titleEl.insertAdjacentHTML("beforeend", title);
+ } catch (e) {
+ console.log("There was an error rendering the title:" + e);
}
- catch(e){
- console.error("Couldn't create Filter Groups in search. ", e)
+ },
+
+ /**
+ * Create the models and views associated with the map and map search
+ * @since 2.22.0
+ */
+ createMap: function () {
+ try {
+ this.mapView = new MapView({ model: this.model.get("map") });
+ } catch (e) {
+ console.error("Couldn't create map in search. ", e);
+ this.toggleMapVisibility(false);
}
-
- },
-
- /**
- * Create the models and views associated with the map and map search
- * @since 2.22.0
- */
- createMap: function(){
- let mapOptions = Object.assign({}, MetacatUI.appModel.get("catalogSearchMapOptions") || {});
- let map = new Map(mapOptions);
-
- //Add a CesiumGeohash layer to the map
- /* let geohashLayer = new CesiumGeohash();
- geohashLayer.
- let assets = map.get("layers");
- assets.add(geohashLayer);
-*/
-
- let geohashLayer = map.get("layers").findWhere({type: "CesiumGeohash"})
-
- //Connect the CesiumGeohash to the SolrResults
- let connector = new GeohashSearchConnector({
- cesiumGeohash: geohashLayer,
- searchResults: this.searchResultsView.searchResults
- });
- connector.startListening();
- this.geohashSearchConnector = connector;
-
- //Set the geohash level for the search
- if( Array.isArray(this.searchResultsView.searchResults.facet) )
- this.searchResultsView.searchResults.facet.push("geohash_" + geohashLayer.get("geohashLevel"));
- else
- this.searchResultsView.searchResults.facet = "geohash_" + geohashLayer.get("geohashLevel");
-
- //Create the Map model and view
- this.mapView = new MapView({ model: map });
- },
-
- /**
- * Renders the Cesium map with a geohash layer
- * @since 2.22.0
- */
- renderMap: function(){
- //Add the map to the page and render it
- this.$(this.mapContainer).empty().append(this.mapView.el);
- this.mapView.render();
- },
-
- /**
- * Linked Data Object for appending the jsonld into the browser DOM
- * @since 2.22.0
- * */
- addLinkedData: function() {
-
- // JSON Linked Data Object
- let elJSON = {
+ },
+
+ /**
+ * Renders the Cesium map with a geohash layer
+ * @since 2.22.0
+ */
+ renderMap: function () {
+ try {
+ // Add the map to the page and render it
+ this.$(this.mapContainer).append(this.mapView.el);
+ this.mapView.render();
+ } catch (e) {
+ console.error("Couldn't render map in search. ", e);
+ this.toggleMapVisibility(false);
+ }
+ },
+
+ /**
+ * Linked Data Object for appending the jsonld into the browser DOM
+ * @since 2.22.0
+ */
+ addLinkedData: function () {
+ try {
+ // JSON Linked Data Object
+ let elJSON = {
"@context": {
- "@vocab": "http://schema.org/"
+ "@vocab": "http://schema.org/",
},
"@type": "DataCatalog",
- }
+ };
- // Find the MN info from the CN Node list
- let members = MetacatUI.nodeModel.get("members"),
+ // Find the MN info from the CN Node list
+ let members = MetacatUI.nodeModel.get("members"),
nodeModelObject;
- for (let i = 0; i < members.length; i++) {
- if (members[i].identifier == MetacatUI.nodeModel.get("currentMemberNode")) {
- nodeModelObject = members[i];
+ for (let i = 0; i < members.length; i++) {
+ if (
+ members[i].identifier ==
+ MetacatUI.nodeModel.get("currentMemberNode")
+ ) {
+ nodeModelObject = members[i];
}
- }
- if (nodeModelObject) {
- // "keywords": "",
- // "provider": "",
+ }
+ if (nodeModelObject) {
+ // "keywords": "", "provider": "",
let conditionalData = {
- "description": nodeModelObject.description,
- "identifier": nodeModelObject.identifier,
- "image": nodeModelObject.logo,
- "name": nodeModelObject.name,
- "url": nodeModelObject.url
- }
+ 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")) {
+ // 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 {
+ } else {
var script = document.getElementById("jsonld");
script.text = JSON.stringify(elJSON);
+ }
+ } catch (e) {
+ console.error("Couldn't add linked data to search. ", e);
}
- },
-
- /**
- * Toggles between map and list search mode
- * @since 2.22.0
- */
- toggleMode: function(){
-
- let classList = document.querySelector("body").classList;
-
- if(this.mode == "map"){
- this.mode = "list";
- classList.remove("mapMode");
- classList.add("listMode")
+ },
+
+ /**
+ * Shows or hide the filters
+ * @param {boolean} show - Optionally provide the desired choice of
+ * whether the filters should be shown (true) or hidden (false). If not
+ * provided, the opposite of the current mode will be used.
+ * @since x.x.x
+ */
+ toggleFiltersVisibility: function (show) {
+ try {
+ const classList = document.querySelector("body").classList;
+
+ // If the new mode is not provided, the new mode is the opposite of
+ // the current mode
+ show = typeof show == "boolean" ? show : !this.filtersVisible;
+ const hideFiltersClass = this.hideFiltersClass;
+
+ if (show) {
+ this.filtersVisible = true;
+ classList.remove(hideFiltersClass);
+ } else {
+ this.filtersVisible = false;
+ classList.add(hideFiltersClass);
+ }
+ this.updateToggleFiltersLabel();
+ } catch (e) {
+ console.error("Couldn't toggle filter visibility. ", e);
}
- else{
- this.mode = "map";
- classList.remove("listMode");
- classList.add("mapMode")
+ },
+
+ /**
+ * Show or hide the map
+ * @param {boolean} show - Optionally provide the desired choice of
+ * whether the filters should be shown (true) or hidden (false). If not
+ * provided, the opposite of the current mode will be used. (Set to true
+ * to show map, false to hide it.)
+ * @since x.x.x
+ */
+ toggleMapVisibility: function (show) {
+ try {
+ // If the new mode is not provided, the new mode is the opposite of
+ // the current mode
+ show = typeof show == "boolean" ? show : !this.mapVisible;
+ const classList = document.querySelector("body").classList;
+ const hideMapClass = this.hideMapClass;
+
+ if (show) {
+ this.mapVisible = true;
+ classList.remove(hideMapClass);
+ } else {
+ this.mapVisible = false;
+ classList.add(hideMapClass);
+ }
+ this.updateToggleMapLabel();
+ } catch (e) {
+ console.error("Couldn't toggle search mode. ", e);
}
-
- },
-
- onClose: function(){
- document.querySelector("body").classList.remove(`catalog-search-body`, `${this.mode}Mode`);
-
- //Remove the JSON-LD from the page
- document.getElementById("jsonld")?.remove();
+ },
+
+ /**
+ * Change the content of the map toggle label to indicate whether
+ * clicking the button will show or hide the map.
+ * @since x.x.x
+ */
+ updateToggleMapLabel: function () {
+ try {
+ const toggleMapLabel = this.el.querySelector(this.toggleMapLabel);
+ const toggleMapButton = this.el.querySelector(this.toggleMapButton);
+ if (this.mapVisible) {
+ if (toggleMapLabel) {
+ toggleMapLabel.innerHTML =
+ 'Hide Map
';
+ }
+ if (toggleMapButton) {
+ toggleMapButton.innerHTML =
+ '
';
+ }
+ } else {
+ if (toggleMapLabel) {
+ toggleMapLabel.innerHTML =
+ '
Show Map
';
+ }
+ if (toggleMapButton) {
+ toggleMapButton.innerHTML = '
';
+ }
+ }
+ } catch (e) {
+ console.log("Couldn't update map toggle. ", e);
+ }
+ },
+
+ /**
+ * Change the content of the filters toggle label to indicate whether
+ * clicking the button will show or hide the filters.
+ * @since x.x.x
+ */
+ updateToggleFiltersLabel: function () {
+ try {
+ const toggleFiltersLabel = this.el.querySelector(
+ this.toggleFiltersLabel
+ );
+ const toggleFiltersButton = this.el.querySelector(
+ this.toggleFiltersButton
+ );
+ if (this.filtersVisible) {
+ if (toggleFiltersLabel) {
+ toggleFiltersLabel.innerHTML =
+ 'Hide Filters
';
+ }
+ if (toggleFiltersButton) {
+ toggleFiltersButton.innerHTML =
+ '
';
+ }
+ } else {
+ if (toggleFiltersLabel) {
+ toggleFiltersLabel.innerHTML =
+ '
Show Filters
';
+ }
+ if (toggleFiltersButton) {
+ toggleFiltersButton.innerHTML =
+ '
';
+ }
+ }
+ } catch (e) {
+ console.log("Couldn't update filters toggle. ", e);
+ }
+ },
+
+ /**
+ * Toggles the map filter on and off
+ * @param {boolean} newSetting - Optionally provide the desired new mode
+ * to switch to. true = limit search to map area, false = do not limit
+ * search to map area. If not provided, the opposite of the current mode
+ * will be used.
+ * @since x.x.x
+ */
+ toggleMapFilter: function (newSetting) {
+ // Make sure the new setting is a boolean
+ newSetting =
+ typeof newSetting != "boolean"
+ ? !this.limitSearchToMapArea // the opposite of the current mode
+ : newSetting; // the provided new mode if it is a boolean
+
+ if (newSetting) {
+ // If true, then the filter should be ON
+ this.model.connectFiltersMap();
+ } else {
+ // If false, then the filter should be OFF
+ this.model.disconnectFiltersMap(true);
+ }
+ this.limitSearchToMapArea = newSetting;
+ },
+
+ /**
+ * Tasks to perform when the view is closed
+ * @since 2.22.0
+ */
+ onClose: function () {
+ try {
+ MetacatUI.appModel.removeCSS(this.cssID);
+ document
+ .querySelector("body")
+ .classList.remove(this.bodyClass, this.hideMapClass);
+
+ // Remove the JSON-LD from the page
+ document.getElementById("jsonld")?.remove();
+ } catch (e) {
+ console.error("Couldn't close search view. ", e);
+ }
+ },
}
-
+ );
});
-
-});
\ No newline at end of file
diff --git a/src/js/views/search/SearchResultView.js b/src/js/views/search/SearchResultView.js
index 5e649ce5f..613b17f00 100644
--- a/src/js/views/search/SearchResultView.js
+++ b/src/js/views/search/SearchResultView.js
@@ -1,378 +1,560 @@
/*global define */
-define(['jquery', 'underscore', 'backbone', 'models/SolrResult', 'models/PackageModel', 'views/CitationView', 'text!templates/resultsItem.html'],
- function($, _, Backbone, SolrResult, Package, CitationView, ResultItemTemplate) {
-
- 'use strict';
-
- // SearchResult View
- // --------------
-
- // The DOM element for a SearchResult item...
- var SearchResultView = Backbone.View.extend({
- tagName: 'div',
- className: 'row-fluid result-row',
-
- // Cache the template function for a single item.
- //template: _.template($('#result-template').html()),
- template: _.template(ResultItemTemplate),
- //Templates
- metricStatTemplate: _.template( "
<%=metricValue%> "),
- loadingTemplate: `
`,
-
- // The DOM events specific to an item.
- events: {
- 'click .result-selection' : 'toggleSelected',
- 'click .download' : 'download'
- },
-
- // The SearchResultView listens for changes to its model, re-rendering. Since there's
- // a one-to-one correspondence between a **SolrResult** and a **SearchResultView** in this
- // app, we set a direct reference on the model for convenience.
- initialize: function (options) {
-
- if(typeof options !== "object"){
- var options = {}
- }
-
- this.listenTo(this.model, 'change', this.render);
- this.listenTo(this.model, 'reset', this.render);
- //this.listenTo(this.model, 'destroy', this.remove);
- //this.listenTo(this.model, 'visible', this.toggleVisible);
-
- if(typeof options.metricsModel !== "undefined")
- this.metricsModel = options.metricsModel;
-
-
- if(typeof options.className !== "undefined") {
- this.className = options.className;
- }
-
- if(typeof options.template !== "undefined") {
- this.template = options.template;
- }
- },
-
- // Re-render the citation of the result item.
- render: function () {
-
- if( !this.model ){
- return;
- }
-
- //Convert the model to JSON and create the result row from the template
- var json = this.model.toJSON();
-
- /* Add other attributes to the JSON to send to the template */
- //Determine if there is a prov trace
- json.hasProv = this.model.hasProvTrace();
-
- // Add flag to add in annotation icon in results item view if appropriate
- json.hasAnnotations = json.sem_annotation &&
- json.sem_annotation.length &&
- json.sem_annotation.length > 0;
-
- json.showAnnotationIndicator = MetacatUI.appModel.get("showAnnotationIndicator");
-
- //Find the member node object
- json.memberNode = _.findWhere(MetacatUI.nodeModel.get("members"), {identifier: this.model.get("datasource")});
-
- //Figure out if this objbect is a collection or portal
- var isCollection = this.model.getType() == "collection" || this.model.getType() == "portal";
-
- //Determine if this metadata doc documents any data files
- if( Array.isArray(json.documents) && json.documents.length ){
- var dataFileIDs = _.without(json.documents, this.model.get("id"), this.model.get("seriesId"), this.model.get("resourceMap"));
- json.numDataFiles = dataFileIDs.length;
- json.dataFilesMessage = "This dataset contains " + json.numDataFiles + " data file";
- if(json.numDataFiles > 1){
- json.dataFilesMessage += "s";
- }
- }
- else{
- json.numDataFiles = 0;
- json.dataFilesMessage = "This dataset doesn't contain any data files";
- }
-
- if( MetacatUI.appModel.get("displayRepoLogosInSearchResults") ){
- //If this result has a logo and it is not a URL, assume it is an ID and create a full URL
- if( json.logo && !json.logo.startsWith("http") ){
- json.logo = MetacatUI.appModel.get("objectServiceUrl") + json.logo;
+define([
+ "jquery",
+ "underscore",
+ "backbone",
+ "models/PackageModel",
+ "views/CitationView",
+ "text!templates/resultsItem.html",
+], function ($, _, Backbone, Package, CitationView, ResultItemTemplate) {
+ "use strict";
+
+ /**
+ * @class SearchResultView
+ * @classdesc A single result item view
+ * @name SearchResultView
+ * @classcategory Views
+ * @extends Backbone.View
+ * @constructor
+ * @screenshot views/search/SearchResultView.png
+ */
+ return Backbone.View.extend(
+ /**
+ * @lends SearchResultView.prototype
+ */ {
+ /**
+ * The tag name for the view's element
+ * @type {string}
+ */
+ tagName: "div",
+
+ /**
+ * The HTML classes to use for this view's element
+ * @type {string}
+ */
+ className: "row-fluid result-row",
+
+ /**
+ * The HTML template to use for this view's main element
+ * @type {underscore.template}
+ */
+ template: _.template(ResultItemTemplate),
+
+ /**
+ * The HTML template to use for the metrics statics
+ * @type {underscore.template}
+ */
+ metricStatTemplate: _.template(
+ `
+ <%=metricValue%>
+ `
+ ),
+
+ /**
+ * The HTML template to use when showing that a result item is loading
+ * @type {string}
+ */
+ loadingTemplate: `
+
`,
+
+ /**
+ * The events this view will listen to and the associated function to
+ * call.
+ * @type {Object}
+ */
+ events: {
+ "click .result-selection": "toggleSelected",
+ "click .download": "download",
+ "mouseover .open-marker": "toggleShowOnMap",
+ "mouseout .open-marker": "toggleShowOnMap",
+ },
+
+ /**
+ * Initialize the view. The SearchResultView listens for changes to its
+ * model, re-rendering. Since there's a one-to-one correspondence between
+ * a **SolrResult** and a **SearchResultView** in this app, we set a
+ * direct reference on the model for convenience.
+ * @param {*} options
+ */
+ initialize: function (options) {
+ if (typeof options !== "object") {
+ var options = {};
}
- var datasourceId = json.memberNode? json.memberNode.identifier : json.datasource,
- currentMemberNode = MetacatUI.appModel.get("nodeId") || datasourceId;
+ this.stopListening(this.model, "change", this.render);
+ this.stopListening(this.model, "reset", this.render);
+ this.listenTo(this.model, "change", this.render);
+ this.listenTo(this.model, "reset", this.render);
+ // this.listenTo(this.model, 'destroy', this.remove);
+ // this.listenTo(this.model, 'visible', this.toggleVisible);
+
+ if (typeof options.metricsModel !== "undefined")
+ this.metricsModel = options.metricsModel;
- //Construct a URL to the profile of this repository
- json.profileURL = (datasourceId == currentMemberNode)?
- MetacatUI.root + "/profile" :
- MetacatUI.appModel.get("dataoneSearchUrl") + "/portals/" + datasourceId.replace("urn:node:", "");
+ if (typeof options.className !== "undefined") {
+ this.className = options.className;
+ }
+
+ if (typeof options.template !== "undefined") {
+ this.template = options.template;
+ }
+ },
+
+ /**
+ * Render or re-render the result item.
+ * @returns {SearchResultView} Returns this view
+ */
+ render: function () {
+ if (!this.model) {
+ return;
+ }
- }
+ // Convert the model to JSON and create the result row from the template
+ var json = this.model.toJSON();
+
+ // Add other attributes to the JSON to send to the template
+
+ // Determine if there is a prov trace
+ json.hasProv = this.model.hasProvTrace();
+
+ // Add flag to add in annotation icon in results item view if
+ // appropriate
+ json.hasAnnotations =
+ json.sem_annotation &&
+ json.sem_annotation.length &&
+ json.sem_annotation.length > 0;
+
+ json.showAnnotationIndicator = MetacatUI.appModel.get(
+ "showAnnotationIndicator"
+ );
+
+ // Find the member node object
+ json.memberNode = _.findWhere(MetacatUI.nodeModel.get("members"), {
+ identifier: this.model.get("datasource"),
+ });
+
+ // Figure out if this object is a collection or portal
+ var isCollection =
+ this.model.getType() == "collection" ||
+ this.model.getType() == "portal";
+
+ // Determine if this metadata doc documents any data files
+ if (Array.isArray(json.documents) && json.documents.length) {
+ var dataFileIDs = _.without(
+ json.documents,
+ this.model.get("id"),
+ this.model.get("seriesId"),
+ this.model.get("resourceMap")
+ );
+ json.numDataFiles = dataFileIDs.length;
+ json.dataFilesMessage =
+ "This dataset contains " + json.numDataFiles + " data file";
+ if (json.numDataFiles > 1) {
+ json.dataFilesMessage += "s";
+ }
+ } else {
+ json.numDataFiles = 0;
+ json.dataFilesMessage = "This dataset doesn't contain any data files";
+ }
- //Create a URL that leads to a view of this object
- json.viewURL = this.model.createViewURL();
+ if (MetacatUI.appModel.get("displayRepoLogosInSearchResults")) {
+ // If this result has a logo and it is not a URL, assume it is an ID
+ // and create a full URL
+ if (json.logo && !json.logo.startsWith("http")) {
+ json.logo = MetacatUI.appModel.get("objectServiceUrl") + json.logo;
+ }
+
+ var datasourceId = json.memberNode
+ ? json.memberNode.identifier
+ : json.datasource,
+ currentMemberNode =
+ MetacatUI.appModel.get("nodeId") || datasourceId;
+
+ // Construct a URL to the profile of this repository
+ json.profileURL =
+ datasourceId == currentMemberNode
+ ? MetacatUI.root + "/profile"
+ : MetacatUI.appModel.get("dataoneSearchUrl") +
+ "/portals/" +
+ datasourceId.replace("urn:node:", "");
+ }
- var resultRow = this.template(json);
- this.$el.html(resultRow);
+ //Create a URL that leads to a view of this object
+ json.viewURL = this.model.createViewURL();
+
+ var resultRow = this.template(json);
+ this.$el.html(resultRow);
+
+ //Create the citation
+ var citation = new CitationView({ metadata: this.model }).render().el;
+ var placeholder = this.$(".citation");
+ if (placeholder.length < 1) this.$el.append(citation);
+ else $(placeholder).replaceWith(citation);
+
+ //Create the OpenURL COinS
+ var span = this.getOpenURLCOinS();
+ this.$el.append(span);
+
+ if (
+ MetacatUI.appModel.get("displayDatasetMetrics") &&
+ this.metricsModel
+ ) {
+ if (this.metricsModel.get("views") !== null) {
+ // Display metrics if the model has already been fetched
+ this.displayMetrics();
+ } else if (this.metricsModel) {
+ // waiting for the fetch() call to succeed.
+ this.listenTo(this.metricsModel, "sync", this.displayMetrics);
+ }
+ }
- //Create the citation
- var citation = new CitationView({metadata: this.model}).render().el;
- var placeholder = this.$(".citation");
- if(placeholder.length < 1) this.$el.append(citation);
- else $(placeholder).replaceWith(citation);
+ if (isCollection) {
+ this.$el.addClass("collection");
+ }
- //Create the OpenURL COinS
- var span = this.getOpenURLCOinS();
- this.$el.append(span);
+ //Save the id in the DOM for later use
+ var id = json.id;
+ this.$el.attr("data-id", id);
+
+ if (this.model.get("abstract")) {
+ var abridgedAbstract =
+ this.model.get("abstract").indexOf(" ", 250) < 0
+ ? this.model.get("abstract")
+ : this.model
+ .get("abstract")
+ .substring(0, this.model.get("abstract").indexOf(" ", 250)) +
+ "...";
+ var content = $(document.createElement("div")).append(
+ $(document.createElement("p")).text(abridgedAbstract)
+ );
+
+ this.$(".popover-this.abstract").popover({
+ trigger: "hover",
+ html: true,
+ content: content,
+ title: "Abstract",
+ placement: "top",
+ container: this.el,
+ });
+ } else {
+ this.$(".popover-this.abstract").addClass("inactive");
+ this.$(".icon.abstract").addClass("inactive");
+ }
- if( MetacatUI.appModel.get("displayDatasetMetrics") && this.metricsModel ){
- if (this.metricsModel.get("views") !== null) {
- // Display metrics if the model has already been fetched
- this.displayMetrics();
+ return this;
+ },
+
+ displayMetrics: function () {
+ try {
+ // If metrics for this object should be hidden, exit the function
+ if (this.model.hideMetrics()) {
+ return;
+ }
+
+ var datasets = this.metricsModel.get("datasets");
+ var downloads = this.metricsModel.get("downloads");
+ var views = this.metricsModel.get("views");
+ var citations = this.metricsModel.get("citations");
+
+ // Initializing the metric counts
+ var viewCount = 0;
+ var downloadCount = 0;
+ var citationCount = 0;
+
+ // Get the individual dataset metics only if the response from Metrics
+ // Service API has non-zero array sizes
+ if (datasets && datasets.length > 0) {
+ var index = datasets.indexOf(this.model.get("id"));
+ viewCount = views[index];
+ downloadCount = downloads[index];
+ citationCount = citations[index];
+ }
+
+ // Generating tool-tip title Citations
+ if (citationCount == 1) {
+ var citationToolTip = citationCount + " citation";
+ } else {
+ var citationToolTip =
+ MetacatUI.appView.numberAbbreviator(citationCount, 1) +
+ " citations";
+ }
+
+ // Downloads
+ if (downloadCount == 1) {
+ var downloadToolTip = downloadCount + " download";
+ } else {
+ var downloadToolTip =
+ MetacatUI.appView.numberAbbreviator(downloadCount, 1) +
+ " downloads";
+ }
+
+ // Views
+ if (viewCount == 1) {
+ var viewToolTip = viewCount + " view";
+ } else {
+ var viewToolTip =
+ MetacatUI.appView.numberAbbreviator(viewCount, 1) + " views";
+ }
+
+ // Replacing the metric total count with the spinning icon.
+ this.$(".resultItem-CitationCount")
+ .html(
+ this.metricStatTemplate({
+ metricValue: MetacatUI.appView.numberAbbreviator(
+ citationCount,
+ 1
+ ),
+ metricIcon: "icon-quote-right",
+ })
+ )
+ .tooltip({
+ placement: "top",
+ trigger: "hover",
+ delay: 800,
+ container: this.el,
+ title: citationToolTip,
+ });
+
+ this.$(".resultItem-DownloadCount")
+ .html(
+ this.metricStatTemplate({
+ metricValue: MetacatUI.appView.numberAbbreviator(
+ downloadCount,
+ 1
+ ),
+ metricIcon: "icon-cloud-download",
+ })
+ )
+ .tooltip({
+ placement: "top",
+ trigger: "hover",
+ delay: 800,
+ container: this.el,
+ title: downloadToolTip,
+ });
+
+ this.$(".resultItem-ViewCount")
+ .html(
+ this.metricStatTemplate({
+ metricValue: MetacatUI.appView.numberAbbreviator(viewCount, 1),
+ metricIcon: "icon-eye-open",
+ })
+ )
+ .tooltip({
+ placement: "top",
+ trigger: "hover",
+ delay: 800,
+ container: this.el,
+ title: viewToolTip,
+ });
+
+ // Removing Citation metric if the citationCount is 0
+ if (citationCount === 0) {
+ this.$(".resultItem-CitationCount").css("visibility", "hidden");
+ }
+
+ // Removing Download metric if the downloadCount is 0
+ if (downloadCount === 0) {
+ this.$(".resultItem-DownloadCount").css("visibility", "hidden");
+ }
+
+ // Removing View metric if the viewCount is 0
+ if (viewCount === 0) {
+ this.$(".resultItem-ViewCount").css("visibility", "hidden");
+ }
+ } catch (e) {
+ console.log("Error displaying metrics: " + e);
}
- else if( this.metricsModel ) {
- // waiting for the fetch() call to succeed.
- this.listenTo(this.metricsModel, "sync", this.displayMetrics);
+ },
+
+ /**
+ * Toggles the selected state of the model
+ */
+ toggleSelected: function () {
+ this.model.toggle();
+ },
+
+ /**
+ * Toggles the showOnMap state of the model when the user hovers over
+ * or leaves the map marker icon
+ * @param {Event} e - The mouseover or mouseout event
+ */
+ toggleShowOnMap: function (e) {
+ this.model.set("showOnMap", e.type === "mouseover")
+ },
+
+ /**
+ * Navigates the app to the metadata page for a result item that was
+ * clicked on
+ * @param {*} e - The click event
+ */
+ routeToMetadata: function (e) {
+ var id = this.model.get("id");
+
+ //If the user clicked on a download button or any element with the class
+ //'stop-route', we don't want to navigate to the metadata
+ if (
+ $(e.target).hasClass("stop-route") ||
+ typeof id === "undefined" ||
+ !id
+ )
+ return;
+
+ MetacatUI.uiRouter.navigate("view/" + encodeURIComponent(id), {
+ trigger: true,
+ });
+ },
+
+ /**
+ * Downloads the data package for a result item that was clicked on
+ * @param {*} e - The click event
+ */
+ download: function (e) {
+ if (
+ MetacatUI.appUserModel.get("loggedIn") &&
+ !this.model.get("isPublic")
+ ) {
+ if (e) {
+ e.preventDefault();
+ var packageId =
+ $(e.target).attr("data-id") || this.model.get("resourceMap");
+ } else var packageId = this.model.get("resourceMap");
+
+ var fileName = this.model.get("fileName") || this.model.get("title");
+
+ // Download the entire package if there is one
+ if (packageId) {
+ // If there is more than one resource map, download all of them
+ if (Array.isArray(packageId)) {
+ for (var i = 0; i < packageId.length; i++) {
+ var pkgFileName = fileName || "Dataset_" + (i + 1);
+
+ //Take off the file extension part of the file name
+ if (pkgFileName.lastIndexOf(".") > 0)
+ pkgFileName = pkgFileName.substring(
+ 0,
+ pkgFileName.lastIndexOf(".")
+ );
+
+ var packageModel = new Package({
+ id: packageId[i],
+ fileName: pkgFileName + ".zip",
+ });
+ packageModel.downloadWithCredentials();
+ }
+ } else {
+ // Take off the file extension part of the file name
+ if (fileName.lastIndexOf(".") > 0)
+ fileName = fileName.substring(0, fileName.lastIndexOf("."));
+
+ // Create a model to represent the package
+ var packageModel = new Package({
+ id: packageId,
+ fileName: fileName + ".zip",
+ });
+ packageModel.downloadWithCredentials();
+ }
+ }
+ // Otherwise just download this solo object
+ else {
+ this.model.downloadWithCredentials();
+ }
+ } else return true;
+ },
+
+ /**
+ * Create ContextObjects in Spans (COinS) for the item. COinS is a method
+ * to embed bibliographic metadata in the HTML code of web pages. This
+ * allows bibliographic software to publish machine-readable bibliographic
+ * items and client reference management software to retrieve
+ * bibliographic metadata. (from wikipedia)
+ * @returns {HTMLSpanElement} - The span element containing the COinS data
+ */
+ getOpenURLCOinS: function () {
+ try {
+ // Create the OpenURL COinS
+ var spanTitle =
+ "ctx_ver=Z39.88-2004&rft_val_fmt=info:ofi/fmt:kev:mtx:dc&rfr_id=info:sid/ocoins.info:generator&rft.type=Dataset";
+
+ if (this.model.get("title"))
+ spanTitle += "&rft.title=" + this.model.get("title");
+ if (this.model.get("origin"))
+ spanTitle += "&rft.creator=" + this.model.get("origin");
+ if (this.model.get("keywords"))
+ spanTitle += "&rft.subject=" + this.model.get("keywords");
+ if (this.model.get("abstract"))
+ spanTitle += "&rft.description=" + this.model.get("abstract");
+ if (this.model.get("datasource"))
+ spanTitle += "&rft.publisher=" + this.model.get("datasource");
+ if (this.model.get("endDate"))
+ spanTitle += "&rft.date=" + this.model.get("endDate");
+ if (this.model.get("formatID"))
+ spanTitle += "&rft.format=" + this.model.get("formatID");
+ if (this.model.get("id"))
+ spanTitle += "&rft.identifier=" + this.model.get("id");
+ if (this.model.get("url"))
+ spanTitle += "&rft.source=" + this.model.get("url");
+ if (this.model.get("northBoundCoord")) {
+ spanTitle +=
+ "&rft.coverage=POLYGON((" +
+ this.model.get("southBoundCoord") +
+ " " +
+ this.model.get("westBoundCoord") +
+ ", " +
+ this.model.get("northBoundCoord") +
+ " " +
+ this.model.get("westBoundCoord") +
+ ", " +
+ this.model.get("northBoundCoord") +
+ " " +
+ this.model.get("eastBoundCoord") +
+ ", " +
+ this.model.get("southBoundCoord") +
+ " " +
+ this.model.get("eastBoundCoord") +
+ "))";
+ }
+
+ spanTitle = encodeURI(spanTitle);
+
+ return $(document.createElement("span"))
+ .attr("title", spanTitle)
+ .addClass("Z3988");
+ } catch (e) {
+ console.log("Error creating COinS: " + e);
+ return $(document.createElement("span"));
}
- }
-
- if( isCollection ){
- this.$el.addClass("collection");
- }
-
- //Save the id in the DOM for later use
- var id = json.id;
- this.$el.attr("data-id", id);
-
- if(this.model.get("abstract")){
- var abridgedAbstract = (this.model.get("abstract").indexOf(" ", 250) < 0) ? this.model.get("abstract") : this.model.get("abstract").substring(0, this.model.get("abstract").indexOf(" ", 250)) + "...";
- var content = $(document.createElement("div"))
- .append($(document.createElement("p")).text(abridgedAbstract));
-
- this.$(".popover-this.abstract").popover({
- trigger: "hover",
- html: true,
- content: content,
- title: "Abstract",
- placement: "top",
- container: this.el
- });
- }
- else{
- this.$(".popover-this.abstract").addClass("inactive");
- this.$(".icon.abstract").addClass("inactive");
- }
-
- return this;
- },
-
- displayMetrics: function() {
-
-
- //If metrics for this object should be hidden, exit the function
- if( this.model.hideMetrics() ) {
- return;
- }
-
- var datasets = this.metricsModel.get("datasets");
- var downloads = this.metricsModel.get("downloads");
- var views = this.metricsModel.get("views");
- var citations = this.metricsModel.get("citations");
-
- // Initializing the metric counts
- var viewCount = 0;
- var downloadCount = 0;
- var citationCount = 0;
-
- // Get the individual dataset metics only if the response from Metrics Service API
- // has non-zero array sizes
- if(datasets && datasets.length > 0) {
- var index = datasets.indexOf(this.model.get("id"));
- viewCount = views[index];
- downloadCount = downloads[index];
- citationCount = citations[index];
- }
-
- // Generating tool-tip title
- // Citations
- if(citationCount == 1){
- var citationToolTip = citationCount + " citation";
- }
- else {
- var citationToolTip = MetacatUI.appView.numberAbbreviator(citationCount,1) + " citations";
- }
-
- // Downloads
- if(downloadCount == 1){
- var downloadToolTip = downloadCount + " download";
- }
- else {
- var downloadToolTip = MetacatUI.appView.numberAbbreviator(downloadCount,1) + " downloads";
- }
-
- // Views
- if(viewCount == 1){
- var viewToolTip = viewCount + " view";
- }
- else {
- var viewToolTip = MetacatUI.appView.numberAbbreviator(viewCount,1) + " views";
- }
-
- // Replacing the metric total count with the spinning icon.
- this.$('.resultItem-CitationCount').html(this.metricStatTemplate({metricValue:MetacatUI.appView.numberAbbreviator(citationCount,1), metricIcon:'icon-quote-right'}))
- .tooltip({
- placement: "top",
- trigger: "hover",
- delay: 800,
- container: this.el,
- title: citationToolTip
- });
-
- this.$('.resultItem-DownloadCount').html(this.metricStatTemplate({metricValue:MetacatUI.appView.numberAbbreviator(downloadCount,1), metricIcon:'icon-cloud-download'}))
- .tooltip({
- placement: "top",
- trigger: "hover",
- delay: 800,
- container: this.el,
- title: downloadToolTip
- });
-
- this.$('.resultItem-ViewCount').html(this.metricStatTemplate({metricValue:MetacatUI.appView.numberAbbreviator(viewCount,1), metricIcon:'icon-eye-open'}))
- .tooltip({
- placement: "top",
- trigger: "hover",
- delay: 800,
- container: this.el,
- title: viewToolTip
- });
-
-
- // Removing Citation metric if the citationCount is 0
- if (citationCount === 0) {
- this.$('.resultItem-CitationCount').css("visibility","hidden");
- }
-
- // Removing Download metric if the downloadCount is 0
- if (downloadCount === 0) {
- this.$('.resultItem-DownloadCount').css("visibility","hidden");
- }
-
- // Removing View metric if the viewCount is 0
- if (viewCount === 0) {
- this.$('.resultItem-ViewCount').css("visibility","hidden");
- }
- },
-
- // Toggle the `"selected"` state of the model.
- toggleSelected: function () {
- this.model.toggle();
- },
-
- routeToMetadata: function(e){
- var id = this.model.get("id");
-
- //If the user clicked on a download button or any element with the class 'stop-route', we don't want to navigate to the metadata
- if ($(e.target).hasClass('stop-route') || (typeof id === "undefined") || !id)
- return;
-
- MetacatUI.uiRouter.navigate('view/' + encodeURIComponent(id), {trigger: true});
- },
-
- download: function(e){
- if(MetacatUI.appUserModel.get("loggedIn") && !this.model.get("isPublic")){
- if(e){
- e.preventDefault();
- var packageId = $(e.target).attr("data-id") || this.model.get("resourceMap");
- }
- else
- var packageId = this.model.get("resourceMap");
-
- var fileName = this.model.get("fileName") || this.model.get("title");
-
- //Download the entire package if there is one
- if(packageId){
-
- //If there is more than one resource map, download all of them
- if(Array.isArray(packageId)){
- for(var i = 0; i
0)
- pkgFileName = pkgFileName.substring(0, pkgFileName.lastIndexOf("."));
-
- var packageModel = new Package({
- id: packageId[i],
- fileName: pkgFileName + ".zip"
- });
- packageModel.downloadWithCredentials();
- }
- }
- else{
- //Take off the file extension part of the file name
- if(fileName.lastIndexOf(".") > 0)
- fileName = fileName.substring(0, fileName.lastIndexOf("."));
-
- //Create a model to represent the package
- var packageModel = new Package({
- id: packageId,
- fileName: fileName + ".zip"
- });
- packageModel.downloadWithCredentials();
- }
- }
- //Otherwise just download this solo object
- else{
- this.model.downloadWithCredentials();
- }
- }
- else
- return true;
- },
-
- getOpenURLCOinS: function(){
- //Create the OpenURL COinS
- var spanTitle = "ctx_ver=Z39.88-2004&rft_val_fmt=info:ofi/fmt:kev:mtx:dc&rfr_id=info:sid/ocoins.info:generator&rft.type=Dataset";
-
- if(this.model.get("title")) spanTitle += "&rft.title=" + this.model.get("title");
- if(this.model.get("origin")) spanTitle += "&rft.creator=" + this.model.get("origin");
- if(this.model.get("keywords")) spanTitle += "&rft.subject=" + this.model.get("keywords");
- if(this.model.get("abstract")) spanTitle += "&rft.description=" + this.model.get("abstract");
- if(this.model.get("datasource")) spanTitle += "&rft.publisher=" + this.model.get("datasource");
- if(this.model.get("endDate")) spanTitle += "&rft.date=" + this.model.get("endDate");
- if(this.model.get("formatID")) spanTitle += "&rft.format=" + this.model.get("formatID");
- if(this.model.get("id")) spanTitle += "&rft.identifier=" + this.model.get("id");
- if(this.model.get("url")) spanTitle += "&rft.source=" + this.model.get("url");
- if(this.model.get("northBoundCoord")){
- spanTitle += "&rft.coverage=POLYGON((" + this.model.get("southBoundCoord") + " " + this.model.get("westBoundCoord") + ", " +
- this.model.get("northBoundCoord") + " " + this.model.get("westBoundCoord") + ", " +
- this.model.get("northBoundCoord") + " " + this.model.get("eastBoundCoord") + ", " +
- this.model.get("southBoundCoord") + " " + this.model.get("eastBoundCoord") + "))";
- }
-
- spanTitle = encodeURI(spanTitle);
-
- return $(document.createElement("span")).attr("title", spanTitle).addClass("Z3988");
- },
-
- loading: function(){
- this.$el.html(this.loadingTemplate);
- },
-
- // Remove the item, destroy the model from *localStorage* and delete its view.
- clear: function () {
- this.model.destroy();
- },
-
- onClose: function(){
- this.clear();
- }
- });
- return SearchResultView;
+ },
+
+ /**
+ * Show the loading view
+ */
+ loading: function () {
+ this.$el.html(this.loadingTemplate);
+ },
+
+ /**
+ * Remove the item, destroy the model from *localStorage* and delete its
+ * view.
+ */
+ clear: function () {
+ this.model.destroy();
+ },
+
+ /**
+ * Functions to perform when the view is closed
+ */
+ onClose: function () {
+ this.clear();
+ },
+ }
+ );
});
diff --git a/src/js/views/search/SearchResultsPagerView.js b/src/js/views/search/SearchResultsPagerView.js
index 4c1026da8..b4309402a 100644
--- a/src/js/views/search/SearchResultsPagerView.js
+++ b/src/js/views/search/SearchResultsPagerView.js
@@ -1,182 +1,297 @@
/*global define */
-define(["backbone"],
-function(Backbone){
-
- "use strict";
-
- /**
- * @class SearchResultsPagerView
- * @name SearchResultsPagerView
- * @classcategory Views/Search
- * @extends Backbone.View
- * @description Renders a simple pager element for a SolrResults collection.
- * @constructor
- * @since 2.22.0
- */
- return Backbone.View.extend(
- /** @lends SearchResultsPagerView.prototype */ {
+define(["backbone"], function (Backbone) {
+ "use strict";
- className: "pager-view search-results-pager-view pagination pagination-centered resultspager",
+ /**
+ * @class SearchResultsPagerView
+ * @name SearchResultsPagerView
+ * @classcategory Views/Search
+ * @extends Backbone.View
+ * @description Renders a simple pager element for a SolrResults collection.
+ * @constructor
+ * @since 2.22.0
+ * @screenshots views/search/SearchResultsPagerView.png
+ */
+ return Backbone.View.extend(
+ /** @lends SearchResultsPagerView.prototype */ {
+ /**
+ * The classes to use for this view's element
+ * @type {string}
+ */
+ className:
+ `pager-view search-results-pager-view pagination ` +
+ `pagination-centered resultspager`,
- tagName: "nav",
+ /**
+ * The HTML tag to use for this view's element
+ * @type {string}
+ */
+ tagName: "nav",
- template: `
-
-
-
- ...
-
+ /**
+ * The HTML to display when no search results are found. This will be
+ * updated by the view.
+ * @type {string}
+ */
+ template: `
+ `,
- /**
- * Constructs and returns a URL string to use for the given page in this pager. It assumes that the URL uses
- * a ".../page/X" structure. To provide a custom URL, override this function.
- * @param {number|string} page
- * @returns {string}
- */
- url: function(page){
- if(typeof page !== "number"){
- try{
- page=parseInt(page);
- }
- catch(e){
- console.error(e);
- return "";
- }
- finally{
- if(isNaN(page)){
- return "";
- }
- }
+ /**
+ * Constructs and returns a URL string to use for the given page in this
+ * pager. It assumes that the URL uses a ".../page/X" structure. To
+ * provide a custom URL, override this function.
+ * @param {number|string} page
+ * @returns {string}
+ */
+ url: function (page) {
+ if (typeof page !== "number") {
+ try {
+ page = parseInt(page);
+ } catch (e) {
+ console.error(e);
+ return "";
+ } finally {
+ if (isNaN(page)) {
+ return "";
}
+ }
+ }
- if( window.location.pathname.includes("page") ){
- return window.location.pathname.replace(/\/page\/\d+/, "/page/" + (page+1));
- }
- else{
- if(window.location.pathname.endsWith("/"))
- return window.location.pathname + "page/" + (page+1);
- else
- return window.location.pathname + "/page/" + (page+1);
- }
- },
-
- /**
- * Constructs and returns the HTML template string for a single page link in the pager
- * @type {function}
- * @param {object} data
- * @returns {string}
- */
- linkTemplate: function(data={
- page: 0,
- pageDisplay: "",
- className: ""
- }){
- let href = `${this.url(data.page)}`;
- if(href.length) href= `href="${href}"`;
- return ` `
- },
-
- /**
- * A SolrResults collection that contains the page data that this Pager displays.
- * @type SolrResults
- */
- searchResults: null,
-
- /**
- * An object literal of events to listen to on this view
- * @type {object}
- */
- events: {
- "click a" : "handleClick"
- },
-
- /**
- * Renders the Pager View
- */
- render: function(){
- this.loading();
- this.el.innerHTML = this.template;
-
- if(this.searchResults){
- this.renderPages();
- this.listenTo(this.searchResults, "reset", this.renderPages);
- }
- },
-
- /**
- * Render the page numbers and links.
- */
- renderPages: function(){
- //Only show pages if the search results have been retrieved (by checking for the header property which is set during parse())
- if(this.searchResults?.header){
- this.removeLoading();
-
- let container = this.el.querySelector("ul"),
- lastPage = this.searchResults.getNumPages(),
- firstPage = 0,
- currentPage = this.searchResults.getCurrentPage();
-
- //Empty the pager container
- container.innerHTML = "";
-
- //Show prev button and the first page number
- if(currentPage > 0){
- container.insertAdjacentHTML("afterbegin", this.linkTemplate({ page: currentPage-1, pageDisplay: "<"}));
- container.insertAdjacentHTML("beforeend", this.linkTemplate({ page: 0, pageDisplay: 1}));
-
- //If there are pages between the first page and the current-2, then show an ellipsis
- if(currentPage-2 > firstPage){
- container.insertAdjacentHTML("beforeend", this.linkTemplate({ page: "", pageDisplay: "...", className: "inactive"}));
- }
- }
-
- //Show the current page plus two on each side
- let pages = [currentPage-2, currentPage-1, currentPage, currentPage+1, currentPage+2];
- for(let page of pages){
- if((page > firstPage && page < lastPage) || page==currentPage){
- container.insertAdjacentHTML("beforeend", this.linkTemplate({ page: page, pageDisplay: page+1, className: page==currentPage? "active" : ""}));
- }
- }
-
- //Show next button and the last page number
- if(currentPage < lastPage){
- //If there are pages between the last page and the current-2, then show an ellipsis
- if(currentPage+2 < lastPage){
- container.insertAdjacentHTML("beforeend", this.linkTemplate({ page: "", pageDisplay: "...", className: "inactive" }));
- }
-
- container.insertAdjacentHTML("beforeend", this.linkTemplate({ page: lastPage, pageDisplay: lastPage+1 }));
- container.insertAdjacentHTML("beforeend", this.linkTemplate({ page: currentPage+1, pageDisplay: ">" }));
- }
+ if (window.location.pathname.includes("page")) {
+ return window.location.pathname.replace(
+ /\/page\/\d+/,
+ "/page/" + (page + 1)
+ );
+ } else {
+ if (window.location.pathname.endsWith("/"))
+ return window.location.pathname + "page/" + (page + 1);
+ else return window.location.pathname + "/page/" + (page + 1);
+ }
+ },
+
+ /**
+ * Constructs and returns the HTML template string for a single page link
+ * in the pager
+ * @type {function}
+ * @param {object} data
+ * @returns {string}
+ */
+ linkTemplate: function (
+ data = {
+ page: 0,
+ pageDisplay: "",
+ className: "",
+ }
+ ) {
+ // Expand the data object into individual variables
+ let { page, pageDisplay, className } = data;
+ let href = `${this.url(data.page)}`;
+ if (href.length) href = `href="${href}"`;
+ return `
+
+
+ `;
+ },
+
+ /**
+ * A SolrResults collection that contains the page data that this Pager
+ * displays.
+ * @type {SolrResults}
+ */
+ searchResults: null,
+
+ /**
+ * An object literal of events to listen to on this view
+ * @type {object}
+ */
+ events: {
+ "click a": "handleClick",
+ },
+
+ /**
+ * Renders the Pager View
+ */
+ render: function () {
+ this.loading();
+ this.el.innerHTML = this.template;
+
+ if (this.searchResults) {
+ this.renderPages();
+ this.stopListening(this.searchResults, "reset error");
+ this.listenTo(this.searchResults, "reset", this.renderPages);
+ // Hide the pager if there is an error with the search results
+ this.listenTo(this.searchResults, "error", this.hide);
+ }
+ },
+
+ /**
+ * Render the page numbers and links.
+ */
+ renderPages: function () {
+ // Only show pages if the search results have been retrieved (by
+ // checking for the header property which is set during parse())
+ if (!this.searchResults || !this.searchResults.header) return;
+ // Ensure that we don't navigate to a page that doesn't exist
+ const numPages = this.searchResults.getNumPages();
+ const currentPage = MetacatUI.appModel.get("page");
+ if (currentPage > numPages) {
+ MetacatUI.appModel.set("page", numPages);
+ this.searchResults.toPage(numPages);
+ return;
+ }
+
+ if (this.searchResults.getNumPages() < 1) {
+ this.hide();
+ return;
+ }
+
+ try {
+ this.show();
+ this.removeLoading();
+
+ let container = this.el.querySelector("ul"),
+ lastPage = this.searchResults.getNumPages(),
+ firstPage = 0,
+ currentPage = this.searchResults.getCurrentPage();
+
+ //Empty the pager container
+ container.innerHTML = "";
+
+ //Show prev button and the first page number
+ if (currentPage > 0) {
+ container.insertAdjacentHTML(
+ "afterbegin",
+ this.linkTemplate({ page: currentPage - 1, pageDisplay: "<" })
+ );
+ container.insertAdjacentHTML(
+ "beforeend",
+ this.linkTemplate({ page: 0, pageDisplay: 1 })
+ );
+
+ //If there are pages between the first page and the current-2, then
+ //show an ellipsis
+ if (currentPage - 2 > firstPage) {
+ container.insertAdjacentHTML(
+ "beforeend",
+ this.linkTemplate({
+ page: "",
+ pageDisplay: "...",
+ className: "inactive",
+ })
+ );
}
- },
+ }
- handleClick: function(evt){
- // Don't hijack the event if the user had Control or Command held down
- if (evt.ctrlKey || evt.metaKey) {
- return;
+ //Show the current page plus two on each side
+ let pages = [
+ currentPage - 2,
+ currentPage - 1,
+ currentPage,
+ currentPage + 1,
+ currentPage + 2,
+ ];
+ for (let page of pages) {
+ if ((page > firstPage && page < lastPage) || page == currentPage) {
+ container.insertAdjacentHTML(
+ "beforeend",
+ this.linkTemplate({
+ page: page,
+ pageDisplay: page + 1,
+ className: page == currentPage ? "active" : "",
+ })
+ );
}
+ }
- evt.preventDefault();
- evt.stopPropagation();
- let page = evt.target.getAttribute("data-page");
- if(this.searchResults){
- this.searchResults.toPage(page);
- MetacatUI.appModel.set("page", page);
- console.log("nav to ", this.url(page));
- MetacatUI.uiRouter.navigate(this.url(page), {trigger: false});
+ //Show next button and the last page number
+ if (currentPage < lastPage) {
+ //If there are pages between the last page and the current-2, then
+ //show an ellipsis
+ if (currentPage + 2 < lastPage) {
+ container.insertAdjacentHTML(
+ "beforeend",
+ this.linkTemplate({
+ page: "",
+ pageDisplay: "...",
+ className: "inactive",
+ })
+ );
}
- },
- loading: function(){
- this.el.classList.add("loading");
- },
+ container.insertAdjacentHTML(
+ "beforeend",
+ this.linkTemplate({ page: lastPage, pageDisplay: lastPage + 1 })
+ );
+ container.insertAdjacentHTML(
+ "beforeend",
+ this.linkTemplate({ page: currentPage + 1, pageDisplay: ">" })
+ );
+ }
+ } catch (e) {
+ console.log("There was an error rendering the pager: ", e);
+ this.hide();
+ }
+ },
+
+ /**
+ * Handles clicks on the pager links
+ * @param {Event} evt
+ */
+ handleClick: function (evt) {
+ // Don't hijack the event if the user had Control or Command held down
+ if (evt.ctrlKey || evt.metaKey) {
+ return;
+ }
- removeLoading: function(){
- this.el.classList.remove("loading");
+ evt.preventDefault();
+ evt.stopPropagation();
+ let page = evt.target.getAttribute("data-page");
+ if (this.searchResults) {
+ this.searchResults.toPage(page);
+ MetacatUI.appModel.set("page", page);
+ MetacatUI.uiRouter.navigate(this.url(page), { trigger: false });
}
+ },
+
+ /**
+ * Shows the loading version of the pager
+ */
+ loading: function () {
+ this.show();
+ this.el.classList.add("loading");
+ },
+
+ /**
+ * Hides the pager
+ * @since x.x.x
+ */
+ hide: function () {
+ this.el.style.visibility = "hidden";
+ },
+
+ /**
+ * Shows the pager
+ * @since x.x.x
+ */
+ show: function () {
+ this.el.style.visibility = "visible";
+ },
- });
-});
\ No newline at end of file
+ /**
+ * Removes the loading version of the pager
+ */
+ removeLoading: function () {
+ this.el.classList.remove("loading");
+ },
+ }
+ );
+});
diff --git a/src/js/views/search/SearchResultsView.js b/src/js/views/search/SearchResultsView.js
index 983c44b26..a6c8f7ad9 100644
--- a/src/js/views/search/SearchResultsView.js
+++ b/src/js/views/search/SearchResultsView.js
@@ -1,178 +1,273 @@
/*global define */
-define(["backbone",
- "collections/SolrResults",
- "views/search/SearchResultView"
- ],
-function(Backbone, SearchResults, SearchResultView){
-
- "use strict";
-
- /**
- * @class SearchResultsView
- * @name SearchResultsView
- * @classcategory Views/Search
- * @extends Backbone.View
- * @since 2.22.0
- * @constructor
- */
- return Backbone.View.extend(
- /** @lends SearchResultsView.prototype */ {
-
- /**
- * The type of View this is
- * @type {string}
- */
- type: "SearchResults",
-
- /**
- * The HTML tag to use for this view's element
- * @type {string}
- */
- tagName: "div",
-
- /**
- * The HTML classes to use for this view's element
- * @type {string}
- */
- className: "search-results-view",
-
- /**
- * The events this view will listen to and the associated function to call.
- * @type {Object}
- */
- events: {
- },
-
+define([
+ "backbone",
+ "collections/SolrResults",
+ "views/search/SearchResultView",
+ "models/MetricsModel",
+], function (Backbone, SearchResults, SearchResultView, MetricsModel) {
+ "use strict";
+
+ /**
+ * @class SearchResultsView
+ * @classdesc A view of search results.
+ * @name SearchResultsView
+ * @classcategory Views/Search
+ * @extends Backbone.View
+ * @since 2.22.0
+ * @constructor
+ * @screenshots views/search/SearchResultsView.png
+ */
+ return Backbone.View.extend(
/**
- * The SolrResults collection that fetches and parses the searches.
- * @type {SolrResults}
- */
- searchResults: null,
-
- /**
- * The HTML to display when no search results are found.
- * @since 2.22.0
- * @type {string}
- */
- noResultsTemplate: `No results found.
`,
-
- render: function(){
-
- if( !this.searchResults ){
- this.setSearchResults();
- }
-
- this.loading();
-
- this.addResultCollection();
-
- this.startListening();
-
- },
-
- /**
- * Sets listeners on the {@link SearchResultsView#searchResults} to change what is displayed in this view.
- */
- startListening: function(){
- this.listenTo(this.searchResults, "add", this.addResultModel);
- this.listenTo(this.searchResults, "reset", this.addResultCollection);
- this.listenTo(this.searchResults, "request", this.loading);
- },
-
- /**
- * Creates and sets the {@link SearchResultsView#searchResults} property.
- * @returns {SolrResults}
- */
- setSearchResults: function(){
- this.searchResults = new SearchResults();
- return this.searchResults;
- },
-
- /**
- * Renders the given {@link SolrResult} model inside this view.
- * @param {SolrResult} searchResult
- */
- addResultModel: function(searchResult){
- try{
- let view = this.createSearchResultView();
- view.model = searchResult;
- this.addResultView(view);
- }
- catch(e){
- console.error("Failed to add a search result to the page: ", e);
- }
- },
-
- /**
- * Renders all {@link SolrResult}s from the {@link SearchResultsView#searchResults} collection.
- */
- addResultCollection: function(){
- if( !this.searchResults )
- return;
- else if( this.searchResults?.header?.get("numFound") == 0){
- this.showNoResults();
- return;
- }
-
- this.empty();
-
- this.searchResults.models.forEach( result => {
- this.addResultModel(result);
- });
- },
-
- /**
- * Adds a Search Result View to the page
- * @param {SearchResultView} view
- */
- addResultView: function(view) {
- this.el.append(view.el);
- view.render();
- },
-
- /**
- * Creates a Search Result View
- */
- createSearchResultView: function(){
- return new SearchResultView();
- },
-
- /**
- * Shows a message when no search results have been found.
- */
- showNoResults: function(){
-
- this.empty();
-
- this.el.replaceChildren(this.noResultsTemplate);
-
- },
-
- empty: function(){
- this.el.innerHTML = "";
- },
-
- /**
- * Renders a skeleton of this view that communicates to the user that it is loading.
- */
- loading: function(){
-
- this.empty();
-
- let rows = this.searchResults.rows,
- i=0;
-
- while(i < rows){
- let view = this.createSearchResultView();
- this.addResultView(view);
- view.loading();
- i++;
- }
-
+ * @lends SearchResultsView.prototype
+ */ {
+ /**
+ * The type of View this is
+ * @type {string}
+ */
+ type: "SearchResults",
+
+ /**
+ * The HTML tag to use for this view's element
+ * @type {string}
+ */
+ tagName: "div",
+
+ /**
+ * The HTML classes to use for this view's element
+ * @type {string}
+ */
+ className: "search-results-view",
+
+ /**
+ * The events this view will listen to and the associated function to
+ * call.
+ * @type {Object}
+ */
+ events: {},
+
+ /**
+ * The SolrResults collection that fetches and parses the searches.
+ * @type {SolrResults}
+ */
+ searchResults: null,
+
+ /**
+ * The HTML to display when no search results are found.
+ * @since 2.22.0
+ * @type {string}
+ */
+ noResultsTemplate: `No results found.
`,
+
+ /**
+ * The metrics model that will be passed to the search result view
+ * @type {MetricsModel}
+ * @since x.x.x
+ */
+ metricsModel: null,
+
+ /**
+ * Render the view.
+ */
+ render: function () {
+ try {
+ if (!this.searchResults) this.setSearchResults();
+
+ if (!this.metricsModel) this.setUpMetrics();
+
+ this.loading();
+
+ if (typeof this.searchResults.getNumFound() == "number") {
+ this.addResultCollection();
+ }
+
+ this.startListening();
+ } catch (e) {
+ console.log("Failed to render search results view.", e);
+ const emailMsg =
+ "There was an error rendering the search results view. " + e;
+ this.showError(null, { responseText: emailMsg });
+ }
+ },
+
+ /**
+ * Removes listeners set by the {@link SearchResultsView#startListening}
+ * method. This is important to prevent zombie listeners from being
+ * created.
+ */
+ removeListeners: function () {
+ this.stopListening(this.searchResults, "add");
+ this.stopListening(this.searchResults, "reset");
+ this.stopListening(this.searchResults, "request");
+ this.listenTo(this.searchResults, "error");
+ },
+
+ /**
+ * Sets listeners on the {@link SearchResultsView#searchResults} to change
+ * what is displayed in this view.
+ */
+ startListening: function () {
+ this.removeListeners();
+ this.listenTo(this.searchResults, "add", this.addResultModel);
+ this.listenTo(this.searchResults, "reset", this.addResultCollection);
+ this.listenTo(this.searchResults, "changing request", this.loading);
+ this.listenTo(this.searchResults, "error", this.showError);
+ this.listenTo(this.searchResults, "add reset", this.updateMetrics);
+ },
+
+ /**
+ * When there is an error fetching the search results, show an alert
+ * message to the user.
+ * @param {SolrResults} searchResults - The collection of search results
+ * @param {Object} response - The response from the server
+ */
+ showError: function (searchResults, response) {
+ console.log("Failed to fetch search results.");
+ if (response) console.log(response);
+
+ const thisRepo = MetacatUI.appModel.get("repositoryName") || "DataONE";
+ const responseText = encodeURIComponent(response.responseText);
+
+ const alert = MetacatUI.appView.showAlert({
+ message: `Oops! It looks like there was a problem retrieving your
+ search results. Please try your search again and contact support
+ if the issue persists. `,
+ classes: `alert-warning`,
+ container: this.el,
+ replaceContents: true,
+ delay: false,
+ remove: false,
+ includeEmail: true,
+ emailBody: `I'm having trouble searching ${thisRepo}.
+ Here is the error message I received: ${responseText}`,
+ });
+ // alert is an HTMLDivElement, add a margin to the left and right:
+ alert[0].style.margin = "0 1rem";
+ },
+
+ /**
+ * Creates and sets the {@link SearchResultsView#searchResults} property.
+ * @returns {SolrResults}
+ */
+ setSearchResults: function () {
+ this.searchResults = new SearchResults();
+ return this.searchResults;
+ },
+
+ /**
+ * Renders the given {@link SolrResult} model inside this view.
+ * @param {SolrResult} searchResult
+ */
+ addResultModel: function (searchResult) {
+ try {
+ let view = this.createSearchResultView();
+ view.model = searchResult;
+ this.addResultView(view);
+ } catch (e) {
+ console.error("Failed to add a search result to the page: ", e);
+ }
+ },
+
+ /**
+ * Renders all {@link SolrResult}s from the
+ * {@link SearchResultsView#searchResults} collection.
+ */
+ addResultCollection: function () {
+ if (!this.searchResults) return;
+ if (this.searchResults.getNumFound() == 0) {
+ this.showNoResults();
+ return;
+ }
+
+ this.empty();
+
+ this.searchResults.models.forEach((result) => {
+ this.addResultModel(result);
+ });
+ },
+
+ /**
+ * Adds a Search Result View to the page
+ * @param {SearchResultView} view
+ */
+ addResultView: function (view) {
+ this.el.append(view.el);
+ view.render();
+ },
+
+ /**
+ * Creates a Search Result View
+ */
+ createSearchResultView: function () {
+ const options = {
+ metricsModel: this.metricsModel,
+ };
+ return new SearchResultView(options);
+ },
+
+ /**
+ * Creates a new MetricsModel if the app is configured to display metrics.
+ * Sets the metrics model on this view. The metrics model is used to
+ * display views, citations, and downloads for each search result.
+ * @since x.x.x
+ * @returns {MetricsModel}
+ */
+ setUpMetrics: function () {
+ if (!MetacatUI.appModel.get("displayDatasetMetrics")) {
+ this.metricsModel = null;
+ return;
+ }
+ this.metricsModel = new MetricsModel({
+ type: "catalog",
+ });
+ return this.metricsModel;
+ },
+
+ /**
+ * Updates the metrics model with the PIDs of the search results and
+ * fetches the metrics.
+ * @since x.x.x
+ * @returns {MetricsModel}
+ */
+ updateMetrics: function () {
+ if (!this.metricsModel) return;
+ this.metricsModel.set("pid_list", this.searchResults.getPIDs());
+ this.metricsModel.fetch();
+ return this.metricsModel;
+ },
+
+ /**
+ * Shows a message when no search results have been found.
+ */
+ showNoResults: function () {
+ this.empty();
+ this.el.innerHTML = this.noResultsTemplate;
+ },
+
+ /**
+ * Removes all child elements from this view.
+ */
+ empty: function () {
+ this.el.innerHTML = "";
+ },
+
+ /**
+ * Renders a skeleton of this view that communicates to the user that it
+ * is loading.
+ */
+ loading: function () {
+ this.empty();
+
+ let rows = this.searchResults.rows,
+ i = 0;
+
+ while (i < rows) {
+ let view = this.createSearchResultView();
+ this.addResultView(view);
+ view.loading();
+ i++;
+ }
+ },
}
-
-
-
- });
-
-});
\ No newline at end of file
+ );
+});
diff --git a/src/js/views/search/SorterView.js b/src/js/views/search/SorterView.js
index b9eb6eb15..f33f6a49f 100644
--- a/src/js/views/search/SorterView.js
+++ b/src/js/views/search/SorterView.js
@@ -1,21 +1,24 @@
/*global define */
define(["backbone"], function (Backbone) {
"use strict";
-/**
- * @class SorterView
- * @classdesc A view that displays a sort controller and sets the sort order on the attached {@link SolrResults} collection.
- * @name SorterView
- * @extends Backbone.View
- * @constructor
- * @since 2.22.0
- * @classcategory Views/Search
- * */
+ /**
+ * @class SorterView
+ * @classdesc A view that displays a sort controller and sets the sort order
+ * on the attached {@link SolrResults} collection.
+ * @name SorterView
+ * @extends Backbone.View
+ * @constructor
+ * @since 2.22.0
+ * @classcategory Views/Search
+ * @screenshot views/search/SorterView.png
+ */
return Backbone.View.extend(
/**
* @lends SorterView.prototype
*/ {
/**
- * A reference to the {@link SolrResults} collection that this sorter displays and controls.
+ * A reference to the {@link SolrResults} collection that this sorter
+ * displays and controls.
* @type {SolrResults}
*/
searchResults: null,
@@ -23,8 +26,10 @@ define(["backbone"], function (Backbone) {
/**
* A list of sort order options to display in this view.
* @typedef {Object} SearchSortOptions
- * @property {string} value The sort value that will be sent directly to the search index in the query string.
- * @property {string} label The name of the sort option that will be shown to the user.
+ * @property {string} value The sort value that will be sent directly to
+ * the search index in the query string.
+ * @property {string} label The name of the sort option that will be shown
+ * to the user.
* @since 2.22.0
*/
sortOptions: [
@@ -34,9 +39,23 @@ define(["backbone"], function (Backbone) {
{ value: "authorSurNameSort+asc", label: "Author (a-z)" },
],
+ /**
+ * The HTML tag to use for this view's element
+ * @type {string}
+ */
tagName: "div",
+
+ /**
+ * The HTML classes to use for this view's element
+ * @type {string}
+ */
className: "sorter-view",
+ /**
+ * The events this view will listen to and the associated function to
+ * call.
+ * @type {Object}
+ */
events: {
change: "setSort",
},
@@ -45,6 +64,9 @@ define(["backbone"], function (Backbone) {
* Renders the view
*/
render: function () {
+ this.stopListening(this.searchResults, "error reset");
+ this.listenTo(this.searchResults, "error reset", this.hideIfNoResults);
+
let select = document.createElement("select");
select.setAttribute("id", "sortOrder");
@@ -59,12 +81,46 @@ define(["backbone"], function (Backbone) {
},
/**
- * Sets the sort order on the {@link SolrResults} when the sort is changed in the UI.
+ * Hides the view if there are no search results.
+ * @since x.x.x
+ */
+ hideIfNoResults: function () {
+ if (
+ !this.searchResults ||
+ !this.searchResults.header ||
+ !this.searchResults.getNumFound()
+ ) {
+ this.hide();
+ } else {
+ this.show();
+ this.render();
+ }
+ },
+
+ /**
+ * Sets the sort order on the {@link SolrResults} when the sort is changed
+ * in the UI.
* @param {Event} e
*/
setSort: function (e) {
this.searchResults.setSort(e.target.value);
},
+
+ /**
+ * Hides the view
+ * @since x.x.x
+ */
+ hide: function () {
+ this.el.style.visibility = "hidden";
+ },
+
+ /**
+ * Shows the view
+ * @since x.x.x
+ */
+ show: function () {
+ this.el.style.visibility = "visible";
+ },
}
);
});
diff --git a/test/README.md b/test/README.md
index c875dc3b1..c49c67fd2 100644
--- a/test/README.md
+++ b/test/README.md
@@ -38,6 +38,8 @@ Adding new tests
----------------
The test suite to be run is defined in `test/config/tests.json`. Paths to new spec test files are added to the `unit` or `integration` array. Tests are run in order, so add the new test in the position that makes most sense.
+âī¸âī¸âī¸ đ **The script `test/scripts/generate-tests.py` can be used to generate new test files with the basic elements set up to start creating unit tests. See the [README](scripts/README.md) for more information.** âī¸âī¸âī¸
+
Running tests
-------------
diff --git a/test/config/tests.json b/test/config/tests.json
index d15c872ff..cffe268a7 100644
--- a/test/config/tests.json
+++ b/test/config/tests.json
@@ -1,10 +1,10 @@
{
"unit": [
- "./js/specs/unit/collections/SolrResults.js",
- "./js/specs/unit/models/Search.js",
- "./js/specs/unit/models/filters/Filter.js",
- "./js/specs/unit/models/filters/NumericFilter.js",
- "./js/specs/unit/models/CitationModel.js",
+ "./js/specs/unit/collections/SolrResults.spec.js",
+ "./js/specs/unit/models/Search.spec.js",
+ "./js/specs/unit/models/filters/Filter.spec.js",
+ "./js/specs/unit/models/filters/NumericFilter.spec.js",
+ "./js/specs/unit/models/CitationModel.spec.js",
"./js/specs/unit/collections/ProjectList.spec.js",
"./js/specs/unit/models/project/Project.spec.js",
"./js/specs/unit/models/metadata/eml211/EML211.spec.js",
@@ -18,11 +18,16 @@
"./js/specs/unit/models/metadata/eml211/EMLOtherEntity.spec.js",
"./js/specs/unit/models/metadata/eml211/EMLParty.spec.js",
"./js/specs/unit/models/metadata/eml211/EMLTemporalCoverage.spec.js",
- "./js/specs/unit/models/maps/assets/CesiumGeohash.js",
- "./js/specs/unit/models/maps/assets/CesiumImagery.js",
+ "./js/specs/unit/models/maps/assets/CesiumImagery.spec.js",
+ "./js/specs/unit/collections/maps/Geohashes.spec.js",
+ "./js/specs/unit/models/connectors/Filters-Map.spec.js",
+ "./js/specs/unit/models/connectors/Filters-Search.spec.js",
+ "./js/specs/unit/models/connectors/Map-Search-Filters.spec.js",
+ "./js/specs/unit/models/connectors/Map-Search.spec.js",
+ "./js/specs/unit/models/filters/SpatialFilter.spec.js",
+ "./js/specs/unit/models/maps/Geohash.spec.js",
+ "./js/specs/unit/models/maps/assets/CesiumGeohash.spec.js",
"./js/specs/unit/common/Utilities.spec.js"
],
- "integration": [
- "./js/specs/integration/collections/SolrResults.js"
- ]
+ "integration": ["./js/specs/integration/collections/SolrResults.spec.js"]
}
diff --git a/test/js/specs/integration/collections/SolrResults.js b/test/js/specs/integration/collections/SolrResults.spec.js
similarity index 100%
rename from test/js/specs/integration/collections/SolrResults.js
rename to test/js/specs/integration/collections/SolrResults.spec.js
diff --git a/test/js/specs/unit/collections/SolrResults.js b/test/js/specs/unit/collections/SolrResults.spec.js
similarity index 100%
rename from test/js/specs/unit/collections/SolrResults.js
rename to test/js/specs/unit/collections/SolrResults.spec.js
diff --git a/test/js/specs/unit/collections/maps/Geohashes.spec.js b/test/js/specs/unit/collections/maps/Geohashes.spec.js
new file mode 100644
index 000000000..e711ed40b
--- /dev/null
+++ b/test/js/specs/unit/collections/maps/Geohashes.spec.js
@@ -0,0 +1,217 @@
+define(["../../../../../../../../src/js/collections/maps/Geohashes"], function (
+ Geohashes
+) {
+ // Configure the Chai assertion library
+ const should = chai.should();
+ const expect = chai.expect;
+
+ describe("Geohashes Test Suite", function () {
+ /* Set up */
+ beforeEach(function () {
+ this.precision_min = 1;
+ this.precision_max = 12;
+ this.geohashes = new Geohashes([], {
+ minPrecision: this.precision_min,
+ maxPrecision: this.precision_max,
+ });
+ });
+
+ /* Tear down */
+ afterEach(function () {
+ this.precision_min = null;
+ this.precision_max = null;
+ this.geohashes = null;
+ });
+
+ describe("Initialization", function () {
+ it("should create a Geohashes instance", function () {
+ new Geohashes().should.be.instanceof(Geohashes);
+ });
+
+ it("should set the min and max precision levels", function () {
+ this.geohashes.MIN_PRECISION.should.equal(this.precision_min);
+ this.geohashes.MAX_PRECISION.should.equal(this.precision_max);
+ });
+ });
+
+ describe("Validation", function () {
+ it("should validate a valid precision", function () {
+ this.geohashes.validatePrecision(5).should.equal(5);
+ });
+
+ it("should fix a precision that is too high", function () {
+ this.geohashes.validatePrecision(13).should.equal(12);
+ });
+
+ it("should fix a precision that is too low", function () {
+ this.geohashes.validatePrecision(-1).should.equal(1);
+ });
+
+ it("should throw an error for an invalid precision if fix is false", function () {
+ expect(() => {
+ this.geohashes.validatePrecision(-1, false);
+ }).to.throw();
+ });
+
+ it("should handle precision arrays", function () {
+ this.geohashes
+ .validatePrecision([1, 2, 3])
+ .should.deep.equal([1, 2, 3]);
+ });
+
+ it("should validate a valid bounding box", function () {
+ const bounds = { north: 80, south: -80, east: 170, west: 160 };
+ this.geohashes.boundsAreValid(bounds).should.be.true;
+ });
+
+ it("should invalidate a bounding box with invalid bounds", function () {
+ const bounds = { north: 80, south: -80, east: 170, west: 190 };
+ this.geohashes.boundsAreValid(bounds).should.be.false;
+ });
+
+ it("should invalidate a bounding box with missing bounds", function () {
+ const bounds = { north: 80, south: -80, east: 170 };
+ this.geohashes.boundsAreValid(bounds).should.be.false;
+ });
+
+ it("should invalidate a bounding box with non-number bounds", function () {
+ const bounds = { north: 80, south: -80, east: 170, west: "west" };
+ this.geohashes.boundsAreValid(bounds).should.be.false;
+ });
+ });
+
+ describe("Bounds", function () {
+ it("should split a bounding box that crosses the prime meridian", function () {
+ const bounds = { north: 80, south: -80, east: -170, west: 170 };
+ const expected = [
+ { north: 80, south: -80, east: 180, west: 170 },
+ { north: 80, south: -80, east: -170, west: -180 },
+ ];
+ this.geohashes.splitBoundingBox(bounds).should.deep.equal(expected);
+ });
+
+ it("should not split a bounding box that does not cross the prime meridian", function () {
+ const bounds = { north: 80, south: -80, east: 170, west: 160 };
+ const expected = [{ north: 80, south: -80, east: 170, west: 160 }];
+ this.geohashes.splitBoundingBox(bounds).should.deep.equal(expected);
+ });
+
+ it("should get the area of a geohash tile", function () {
+ const precision = 5;
+ const expected = 0.0019311904907226562;
+ this.geohashes.getGeohashArea(precision).should.equal(expected);
+ });
+
+ it("should get the area of a geohash tile for a range of precisions", function () {
+ const minPrecision = 1;
+ const maxPrecision = 3;
+ const area1 = (180 * 360) / 32;
+ const area2 = area1 / 32;
+ const area3 = area2 / 32;
+ const expected = {
+ 1: area1,
+ 2: area2,
+ 3: area3,
+ };
+ this.geohashes
+ .getGeohashAreas(minPrecision, maxPrecision)
+ .should.deep.equal(expected);
+ });
+
+ it("should get the area of the world", function () {
+ const bounds = { north: 90, south: -90, east: 180, west: -180 };
+ const expected = 360 * 180;
+ this.geohashes.getBoundingBoxArea(bounds).should.equal(expected);
+ });
+
+ it("should get the area of a small bounding box", function () {
+ const bounds = { north: 45, south: 44, east: 45, west: 44 };
+ const expected = 1;
+ this.geohashes.getBoundingBoxArea(bounds).should.equal(expected);
+ });
+ });
+
+ describe("Precision", function () {
+ it("should get the max precision for a small area", function () {
+ const area = 1;
+ const maxGeohashes = Infinity;
+ const expected = 12;
+ this.geohashes
+ .getMaxPrecision(area, maxGeohashes)
+ .should.equal(expected);
+ });
+
+ it("should get the max precision for a large area", function () {
+ const area = 360 * 180;
+ const maxGeohashes = 32 * 32;
+ const expected = 2;
+ this.geohashes
+ .getMaxPrecision(area, maxGeohashes)
+ .should.equal(expected);
+ });
+
+ it("should get the min precision for a small area", function () {
+ const area = 1;
+ const expected = 3;
+ this.geohashes.getMinPrecision(area).should.equal(expected);
+ });
+
+ it("should get the min precision for a large area", function () {
+ const area = 360 * 180;
+ const expected = 1;
+ this.geohashes.getMinPrecision(area).should.equal(expected);
+ });
+
+ it("should get the unique precision levels in the collection", function () {
+ const geohashes = new Geohashes([
+ { hashString: "a" },
+ { hashString: "ab" },
+ { hashString: "abc" },
+ ]);
+ const expected = [1, 2, 3];
+ geohashes.getPrecisions().should.deep.equal(expected);
+ });
+ });
+
+ describe("Geohash Generation & Retrieval", function () {
+ it("should return the geohashes as a GeoJSON FeatureCollection", function () {
+ const geohashes = new Geohashes([{ hashString: "gw" }]);
+ const expected = {
+ type: "FeatureCollection",
+ features: [
+ {
+ type: "Feature",
+ geometry: {
+ type: "Polygon",
+ coordinates: [
+ [
+ [-22.5, 78.75],
+ [-11.25, 78.75],
+ [-11.25, 84.375],
+ [-22.5, 84.375],
+ [-22.5, 78.75],
+ ],
+ ],
+ },
+ properties: {
+ hashString: "gw",
+ },
+ },
+ ],
+ };
+ geohashes.toGeoJSON().should.deep.equal(expected);
+ });
+ it("should find a geohash that contains the provided hashString", function () {
+ const geohashes = new Geohashes([{ hashString: "gw" }]);
+ const expected = geohashes.at(0);
+ geohashes.getContainingGeohash("gwa").should.deep.equal(expected);
+ });
+
+ it("should find a geohash that is the provided hashString", function () {
+ const geohashes = new Geohashes([{ hashString: "gw" }]);
+ const expected = geohashes.at(0);
+ geohashes.getContainingGeohash("gw").should.deep.equal(expected);
+ });
+ });
+ });
+});
diff --git a/test/js/specs/unit/models/CitationModel.js b/test/js/specs/unit/models/CitationModel.spec.js
similarity index 100%
rename from test/js/specs/unit/models/CitationModel.js
rename to test/js/specs/unit/models/CitationModel.spec.js
diff --git a/test/js/specs/unit/models/Search.js b/test/js/specs/unit/models/Search.spec.js
similarity index 100%
rename from test/js/specs/unit/models/Search.js
rename to test/js/specs/unit/models/Search.spec.js
diff --git a/test/js/specs/unit/models/connectors/Filters-Map.spec.js b/test/js/specs/unit/models/connectors/Filters-Map.spec.js
new file mode 100644
index 000000000..bd14a86dc
--- /dev/null
+++ b/test/js/specs/unit/models/connectors/Filters-Map.spec.js
@@ -0,0 +1,85 @@
+define([
+ "../../../../../../../../src/js/models/connectors/Filters-Map",
+], function (FiltersMap) {
+ // Configure the Chai assertion library
+ var should = chai.should();
+ var expect = chai.expect;
+
+ describe("FiltersMap Test Suite", function () {
+ /* Set up */
+ beforeEach(function () {
+ this.filtersMap = new FiltersMap();
+ });
+
+ /* Tear down */
+ afterEach(function () {
+ this.filtersMap = null;
+ });
+
+ describe("Initialization", function () {
+ it("should create a FiltersMap instance", function () {
+ new FiltersMap().should.be.instanceof(FiltersMap);
+ });
+
+ it("should add a spatial filter to the Filters collection", function () {
+ const filters = this.filtersMap.get("filters");
+ filters.length.should.equal(1);
+ filters.at(0).get("filterType").should.equal("SpatialFilter");
+ });
+
+ it("should set the spatialFilters on the model", function () {
+ const spatialFilters = this.filtersMap.get("spatialFilters");
+ spatialFilters.length.should.equal(1);
+ spatialFilters[0].get("filterType").should.equal("SpatialFilter");
+ });
+ });
+
+ describe("Spatial Filter", function () {
+ it("should remove the spatial filter from the Filters collection", function () {
+ this.filtersMap.removeSpatialFilter();
+ const filters = this.filtersMap.get("filters");
+ filters.length.should.equal(0);
+ });
+
+ it("should reset the spatial filter values to their defaults", function () {
+ const spatialFilters = this.filtersMap.get("spatialFilters");
+ spatialFilters[0].set("values", ["test"]);
+ this.filtersMap.resetSpatialFilter();
+ spatialFilters[0].get("values").should.deep.equal([]);
+ });
+
+ it("should update the spatial filter extent", function () {
+ const map = this.filtersMap.get("map");
+ const spatialFilters = this.filtersMap.get("spatialFilters");
+ const extent = { north: 1, south: 2, east: 3, west: 4 };
+ map.set("currentViewExtent", extent);
+ this.filtersMap.updateSpatialFilters();
+ spatialFilters[0].get("north").should.equal(1);
+ spatialFilters[0].get("south").should.equal(2);
+ spatialFilters[0].get("east").should.equal(3);
+ spatialFilters[0].get("west").should.equal(4);
+ });
+ });
+
+ describe("Connect/Disconnect", function () {
+ it("should connect to the map", function () {
+ this.filtersMap.connect();
+ this.filtersMap.get("isConnected").should.equal(true);
+ });
+
+ it("should disconnect from the map", function () {
+ this.filtersMap.connect();
+ this.filtersMap.disconnect();
+ this.filtersMap.get("isConnected").should.equal(false);
+ });
+
+ it("should disconnect from the map and reset the spatial filter", function () {
+ this.filtersMap.connect();
+ this.filtersMap.disconnect(true);
+ this.filtersMap.get("isConnected").should.equal(false);
+ const spatialFilters = this.filtersMap.get("spatialFilters");
+ spatialFilters[0].get("values").should.deep.equal([]);
+ });
+ });
+ });
+});
diff --git a/test/js/specs/unit/models/connectors/Filters-Search.spec.js b/test/js/specs/unit/models/connectors/Filters-Search.spec.js
new file mode 100644
index 000000000..a22bb1e22
--- /dev/null
+++ b/test/js/specs/unit/models/connectors/Filters-Search.spec.js
@@ -0,0 +1,44 @@
+define([
+ "../../../../../../../../src/js/models/connectors/Filters-Search",
+], function (FiltersSearch) {
+ // Configure the Chai assertion library
+ var should = chai.should();
+ var expect = chai.expect;
+
+ describe("FiltersSearch Test Suite", function () {
+ /* Set up */
+ beforeEach(function () {
+ this.filtersSearch = new FiltersSearch();
+ });
+
+ /* Tear down */
+ afterEach(function () {
+ this.filtersSearch = null;
+ });
+
+ describe("Initialization", function () {
+ it("should create a FiltersSearch instance", function () {
+ new FiltersSearch().should.be.instanceof(FiltersSearch);
+ });
+ });
+
+ describe("Connect/Disconnect", function () {
+ it("should connect to the search results", function () {
+ this.filtersSearch.connect();
+ this.filtersSearch.get("isConnected").should.equal(true);
+ });
+
+ it("should disconnect from the search results", function () {
+ this.filtersSearch.connect();
+ this.filtersSearch.disconnect();
+ this.filtersSearch.get("isConnected").should.equal(false);
+ });
+ });
+ describe("Trigger Search", function () {
+ it("should trigger a search", function () {
+ // TODO: Figure out how to test this
+ this.filtersSearch.triggerSearch();
+ });
+ });
+ });
+});
diff --git a/test/js/specs/unit/models/connectors/Map-Search-Filters.spec.js b/test/js/specs/unit/models/connectors/Map-Search-Filters.spec.js
new file mode 100644
index 000000000..fef7699ea
--- /dev/null
+++ b/test/js/specs/unit/models/connectors/Map-Search-Filters.spec.js
@@ -0,0 +1,82 @@
+define([
+ "../../../../../../../../src/js/models/connectors/Map-Search-Filters",
+ "../../../../../../../../src/js/models/maps/Map",
+ "../../../../../../../../src/js/collections/SolrResults",
+ "../../../../../../../../src/js/collections/Filters",
+], function (MapSearchFilters, Map, SolrResults, Filters) {
+ // Configure the Chai assertion library
+ var should = chai.should();
+ var expect = chai.expect;
+
+ describe("MapSearchFilters Test Suite", function () {
+ /* Set up */
+ beforeEach(function () {
+ this.mapSearchFilters = new MapSearchFilters();
+ });
+
+ /* Tear down */
+ afterEach(function () {
+ this.mapSearchFilters = null;
+ });
+
+ describe("Initialization", function () {
+ it("should create a MapSearchFilters instance", function () {
+ new MapSearchFilters().should.be.instanceof(MapSearchFilters);
+ });
+
+ it("should set a map, search results, and filters", function () {
+ this.mapSearchFilters.get("map").type.should.equal("MapModel");
+ this.mapSearchFilters
+ .get("searchResults")
+ .type.should.equal("SolrResults");
+ this.mapSearchFilters.get("filters").type.should.equal("Filters");
+ });
+
+ it("should set up connectors", function () {
+ const connectors = this.mapSearchFilters.getConnectors();
+ connectors.length.should.equal(3);
+ connectors[0].type.should.equal("MapSearchConnector");
+ connectors[1].type.should.equal("FiltersSearchConnector");
+ connectors[2].type.should.equal("FiltersMapConnector");
+ });
+ });
+
+ describe("Connect/Disconnect", function () {
+ it("should connect to the search results", function () {
+ this.mapSearchFilters.connect();
+ const connectors = this.mapSearchFilters.getConnectors();
+ connectors.forEach((connector) => {
+ connector.get("isConnected").should.equal(true);
+ });
+ });
+
+ it("should disconnect from the search results", function () {
+ this.mapSearchFilters.connect();
+ this.mapSearchFilters.disconnect();
+ const connectors = this.mapSearchFilters.getConnectors();
+ connectors.forEach((connector) => {
+ connector.get("isConnected").should.equal(false);
+ });
+ });
+ });
+
+ describe("Coordinate MoveEnd Search", function () {
+ it("should coordinate the moveEnd search", function () {
+ this.mapSearchFilters.coordinateMoveEndSearch();
+ const connectors = this.mapSearchFilters.getMapConnectors();
+ connectors.forEach((connector) => {
+ expect(connector.get("onMoveEnd") === null).to.equal(true);
+ });
+ });
+
+ it("should reset the moveEnd search behaviour", function () {
+ this.mapSearchFilters.coordinateMoveEndSearch();
+ this.mapSearchFilters.resetMoveEndSearch();
+ const connectors = this.mapSearchFilters.getMapConnectors();
+ connectors.forEach((connector) => {
+ expect(typeof connector.get("onMoveEnd")).to.equal("function");
+ });
+ });
+ });
+ });
+});
diff --git a/test/js/specs/unit/models/connectors/Map-Search.spec.js b/test/js/specs/unit/models/connectors/Map-Search.spec.js
new file mode 100644
index 000000000..9a2402771
--- /dev/null
+++ b/test/js/specs/unit/models/connectors/Map-Search.spec.js
@@ -0,0 +1,72 @@
+define([
+ "../../../../../../../../src/js/models/connectors/Map-Search",
+ "../../../../../../../../src/js/models/maps/assets/CesiumGeohash",
+], function (MapSearch, CesiumGeohash) {
+ // Configure the Chai assertion library
+ var should = chai.should();
+ var expect = chai.expect;
+
+ describe("MapSearch Test Suite", function () {
+ /* Set up */
+ beforeEach(function () {
+ this.mapSearch = new MapSearch();
+ });
+
+ /* Tear down */
+ afterEach(function () {
+ this.mapSearch = null;
+ });
+
+ describe("Initialization", function () {
+ it("should create a MapSearch instance", function () {
+ new MapSearch().should.be.instanceof(MapSearch);
+ });
+ });
+
+ describe("Connect/Disconnect", function () {
+ it("should connect", function () {
+ this.mapSearch.connect();
+ this.mapSearch.get("isConnected").should.be.true;
+ });
+
+ it("should disconnect", function () {
+ this.mapSearch.connect();
+ this.mapSearch.disconnect();
+ this.mapSearch.get("isConnected").should.be.false;
+ });
+ });
+
+ describe("Geohash Layer", function () {
+ it("should get the geohash layer", function () {
+ console.log(this.mapSearch.get("geohashLayer"));
+ this.mapSearch.get("geohashLayer").type.should.equal("CesiumGeohash");
+ });
+
+ it("should set the geohash layer", function () {
+ var geohashLayer = new CesiumGeohash();
+ this.mapSearch.set("geohashLayer", geohashLayer);
+ this.mapSearch.get("geohashLayer").should.equal(geohashLayer);
+ });
+ });
+
+ describe("Geohash Counts", function () {
+ it("should get the geohash counts", function () {
+ this.mapSearch.getGeohashCounts().should.deep.equal([]);
+ });
+
+ it("should get the geohash counts", function () {
+ var searchResults = {
+ facetCounts: {
+ geohash_9: ["hash1", 1, "hash2", 2],
+ geohash_8: ["hash3", 3, "hash4", 4],
+ },
+ };
+ this.mapSearch.set("searchResults", searchResults);
+ this.mapSearch
+ .getGeohashCounts()
+ .should.deep.equal(["hash1", 1, "hash2", 2, "hash3", 3, "hash4", 4]);
+ });
+ });
+
+ });
+});
diff --git a/test/js/specs/unit/models/filters/Filter.js b/test/js/specs/unit/models/filters/Filter.spec.js
similarity index 100%
rename from test/js/specs/unit/models/filters/Filter.js
rename to test/js/specs/unit/models/filters/Filter.spec.js
diff --git a/test/js/specs/unit/models/filters/NumericFilter.js b/test/js/specs/unit/models/filters/NumericFilter.spec.js
similarity index 100%
rename from test/js/specs/unit/models/filters/NumericFilter.js
rename to test/js/specs/unit/models/filters/NumericFilter.spec.js
diff --git a/test/js/specs/unit/models/filters/SpatialFilter.spec.js b/test/js/specs/unit/models/filters/SpatialFilter.spec.js
new file mode 100644
index 000000000..566c8dbf4
--- /dev/null
+++ b/test/js/specs/unit/models/filters/SpatialFilter.spec.js
@@ -0,0 +1,21 @@
+define([
+ "../../../../../../../../src/js/models/filters/SpatialFilter",
+], function (Spatialfilter) {
+ // Configure the Chai assertion library
+ var should = chai.should();
+ var expect = chai.expect;
+
+ describe("Spatialfilter Test Suite", function () {
+ /* Set up */
+ beforeEach(function () {});
+
+ /* Tear down */
+ afterEach(function () {});
+
+ describe("Initialization", function () {
+ it("should create a Spatialfilter instance", function () {
+ new Spatialfilter().should.be.instanceof(Spatialfilter);
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/test/js/specs/unit/models/maps/Geohash.spec.js b/test/js/specs/unit/models/maps/Geohash.spec.js
new file mode 100644
index 000000000..75f0c45a3
--- /dev/null
+++ b/test/js/specs/unit/models/maps/Geohash.spec.js
@@ -0,0 +1,137 @@
+define(["../../../../../../../../src/js/models/maps/Geohash"], function (
+ Geohash
+) {
+ // Configure the Chai assertion library
+ var should = chai.should();
+ var expect = chai.expect;
+
+ describe("Geohash Test Suite", function () {
+ /* Set up */
+ beforeEach(function () {
+ this.hashString = "9q8yy";
+ this.properties = {
+ count: 21,
+ };
+ this.geohash = new Geohash({
+ hashString: this.hashString,
+ properties: this.properties,
+ });
+ });
+
+ /* Tear down */
+ afterEach(function () {
+ this.geohash = null;
+ });
+
+ describe("Initialization", function () {
+ it("should create a Geohash instance", function () {
+ new Geohash().should.be.instanceof(Geohash);
+ });
+ });
+
+ describe("Property Handling", function () {
+ it("should set the hashString property", function () {
+ this.geohash.get("hashString").should.equal(this.hashString);
+ });
+
+ it("should set the properties property", function () {
+ this.geohash.get("properties").should.equal(this.properties);
+ });
+
+ it("should identify properties", function () {
+ this.geohash.isProperty("count").should.be.true;
+ });
+
+ it("should get properties", function () {
+ this.geohash.getProperty("count").should.equal(21);
+ });
+
+ it("should add properties", function () {
+ this.geohash.addProperty("name", "test");
+ this.geohash.getProperty("name").should.equal("test");
+ });
+
+ it("should remove properties", function () {
+ this.geohash.removeProperty("count");
+ expect(this.geohash.getProperty("count")).to.be.null;
+ });
+ });
+
+ describe("Geometry", function () {
+ it("should get the bounds of the geohash", function () {
+ const bounds = this.geohash.getBounds();
+ bounds.should.be.an("array");
+ bounds.length.should.equal(4);
+ const expectedBounds = [
+ 37.7490234375, -122.431640625, 37.79296875, -122.3876953125,
+ ];
+ bounds.should.deep.equal(expectedBounds);
+ });
+
+ it("should get the center point of the geohash", function () {
+ const point = this.geohash.getPoint();
+ point.should.be.an("object");
+ point.latitude.should.equal(37.77099609375);
+ point.longitude.should.equal(-122.40966796875);
+ });
+
+ it("should get the precision of the geohash", function () {
+ this.geohash.getPrecision().should.equal(this.hashString.length);
+ });
+
+ it("should get the 32 child geohashes", function () {
+ const childGeohashes = this.geohash.getChildGeohashes();
+ childGeohashes.should.be.an("array");
+ childGeohashes.length.should.equal(32);
+ childGeohashes.forEach((geohash) => {
+ geohash.should.be.an.instanceof(Geohash);
+ // Every child should have a precision one greater than the parent.
+ geohash.getPrecision().should.equal(this.geohash.getPrecision() + 1);
+ // Every child should start with the same hashString as the parent.
+ geohash.get("hashString").startsWith(this.geohash.get("hashString"));
+ });
+ });
+
+ it("should get the parent geohash", function () {
+ const parentGeohash = this.geohash.getParentGeohash();
+ parentGeohash.should.be.an.instanceof(Geohash);
+ // The parent should have a precision one less than the child.
+ parentGeohash
+ .getPrecision()
+ .should.equal(this.geohash.getPrecision() - 1);
+ // The parent should start with the same hashString as the child.
+ this.geohash
+ .get("hashString")
+ .startsWith(parentGeohash.get("hashString")).should.be.true;
+ });
+
+ it("should convert geodetic coordinates to ECEF", function () {
+ const coord = [-122.40966796875, 37.77099609375];
+ const ecef = this.geohash.geodeticToECEF(coord);
+ ecef.should.be.an("array");
+ ecef.length.should.equal(3);
+ ecef[0].should.be.a("number");
+ ecef[1].should.be.a("number");
+ ecef[2].should.be.a("number");
+ });
+ });
+
+ describe("Serialization", function () {
+ it("should serialize to JSON", function () {
+ const json = this.geohash.toJSON();
+ json.should.be.an("object");
+ json.hashString.should.equal(this.hashString);
+ json.properties.should.deep.equal(this.properties);
+ });
+
+ it("should serialize to CZML", function () {
+ const czml = this.geohash.toCZML();
+ czml.should.be.an("array");
+ czml.length.should.equal(1);
+ czml[0].should.be.an("object");
+ czml[0].id.should.equal(this.hashString);
+ czml[0].polygon.should.be.an("object");
+ });
+ });
+ });
+});
diff --git a/test/js/specs/unit/models/maps/assets/CesiumGeohash.js b/test/js/specs/unit/models/maps/assets/CesiumGeohash.js
deleted file mode 100644
index 3586dcde7..000000000
--- a/test/js/specs/unit/models/maps/assets/CesiumGeohash.js
+++ /dev/null
@@ -1,40 +0,0 @@
-define([
- "../../../../../../../../../src/js/models/maps/assets/CesiumGeohash",
- "../../../../../../../../../src/components/cesium/Cesium"
- ], function (CesiumGeohash, Cesium) {
-
- // Configure the Chai assertion library
- var should = chai.should();
- var expect = chai.expect;
-
- describe("CesiumGeohash Test Suite", function () {
- /* Set up */
- beforeEach(function () {
-
- })
-
- /* Tear down */
- afterEach(function () {
-
- })
-
- describe("Initialization", function () {
- it("should create a CesiumGeohash model", function () {
- (new CesiumGeohash()).should.be.instanceof(CesiumGeohash)
- });
- });
-
- describe("Working with Geohashes", function () {
-
- it("gets the geohash level", function () {
-
- let g = new CesiumGeohash();
- g.setGeohashLevel(20000);
- g.get("geohashLevel").should.eql(5);
-
- })
-
- });
-
- });
- });
\ No newline at end of file
diff --git a/test/js/specs/unit/models/maps/assets/CesiumGeohash.spec.js b/test/js/specs/unit/models/maps/assets/CesiumGeohash.spec.js
new file mode 100644
index 000000000..9eab3db57
--- /dev/null
+++ b/test/js/specs/unit/models/maps/assets/CesiumGeohash.spec.js
@@ -0,0 +1,110 @@
+define([
+ "../../../../../../../../src/js/models/maps/assets/CesiumGeohash",
+ "../../../../../../../../src/js/collections/maps/Geohashes",
+ "../../../../../../../../src/js/models/maps/Map",
+], function (CesiumGeohash, Geohashes) {
+ // Configure the Chai assertion library
+ var should = chai.should();
+ var expect = chai.expect;
+
+ describe("CesiumGeohash Test Suite", function () {
+ /* Set up */
+ beforeEach(function () {
+ this.map = new Map();
+ this.model = new CesiumGeohash();
+ this.model.set("mapModel", this.map);
+ });
+
+ /* Tear down */
+ afterEach(function () {
+ this.model = null;
+ });
+
+ describe("Initialization", function () {
+ it("should create a CesiumGeohash instance", function () {
+ new CesiumGeohash().should.be.instanceof(CesiumGeohash);
+ });
+
+ it("should configure labels", function () {
+ this.model.get("type").should.equal("CzmlDataSource");
+ const noLabelModel = new CesiumGeohash({ showLabels: false });
+ noLabelModel.get("type").should.equal("GeoJsonDataSource");
+ });
+ });
+
+ describe("getGeohashes", function () {
+ it("should return the geohashes", function () {
+ const geohashes = this.model.getGeohashes();
+ geohashes.type.should.equal("Geohashes");
+ });
+
+ it("should return the geohashes in the current extent", function () {
+ const geohashes = this.model.getGeohashesForExtent();
+ geohashes.type.should.equal("Geohashes");
+ });
+ });
+
+ describe("Output Formats", function () {
+ it("should return the GeoJSON", function () {
+ const geojson = this.model.getGeoJSON();
+ geojson.type.should.equal("FeatureCollection");
+ });
+
+ it("should return the CZML", function () {
+ const czml = this.model.getCZML();
+ czml.should.be.an("array");
+ czml[0].should.have.property("id");
+ czml[0].should.have.property("name");
+ czml[0].should.have.property("version");
+ });
+ });
+
+ describe("Cesium", function () {
+ it("should create a Cesium model", function () {
+ this.model.get("cesiumModel").should.be.an("object");
+ });
+ });
+
+ describe("Geohash Layer Specific", function () {
+ it("should replace the geohashes", function () {
+ this.model.replaceGeohashes([
+ { hashString: "9q" },
+ { hashString: "9r" },
+ { hashString: "9x" },
+ ]);
+ this.model.get("geohashes").length.should.equal(3);
+ });
+
+ it("should empty the geohashes", function () {
+ this.model.replaceGeohashes();
+ this.model.get("geohashes").length.should.equal(0);
+ });
+
+ it("should get the precision", function () {
+ this.model.replaceGeohashes();
+ this.model.set("maxGeoHashes", 32);
+ this.map.set("currentViewExtent", {
+ north: 90,
+ south: -90,
+ east: 180,
+ west: -180,
+ });
+ this.model.getPrecision().should.equal(1);
+ });
+
+ it("should get the property of interest", function () {
+ this.model.getPropertyOfInterest().should.equal("count");
+ });
+
+ it("should calculate the min and max vals for the color palette", function () {
+ this.model.replaceGeohashes([
+ { hashString: "9q", count: 10 },
+ { hashString: "9r", count: 50 },
+ ]);
+ this.model.updateColorRangeValues();
+ this.model.get("colorPalette").get("minVal").should.equal(10);
+ this.model.get("colorPalette").get("maxVal").should.equal(50);
+ });
+ });
+ });
+});
diff --git a/test/js/specs/unit/models/maps/assets/CesiumImagery.js b/test/js/specs/unit/models/maps/assets/CesiumImagery.spec.js
similarity index 100%
rename from test/js/specs/unit/models/maps/assets/CesiumImagery.js
rename to test/js/specs/unit/models/maps/assets/CesiumImagery.spec.js
diff --git a/test/scripts/README.md b/test/scripts/README.md
new file mode 100644
index 000000000..df2a7d870
--- /dev/null
+++ b/test/scripts/README.md
@@ -0,0 +1,24 @@
+# Test Generator Script
+
+The `generate-tests.py` script helps create unit test starter files for MetacatUI. It generates test files with the basic structure and imports the necessary modules based on the file paths provided in the script.
+
+## How to use
+
+1. Update the `test_files` list in the `generate-tests.py` script with the relative paths of the files you want to create tests for (e.g., "collections/maps/Geohashes.js").
+
+2. Run the script from the "test/scripts" directory:
+
+```bash
+cd test/scripts
+python3 generate-tests.py
+```
+
+3. The script will create test files in the "test/js/specs/unit" directory with the appropriate folder structure. It will also print the paths of the created test files, which you'll need to add to the `test/config/tests.json` file.
+
+4. Open the generated test files and add your test cases as needed.
+
+## Notes
+
+- The script assumes that the project repository is cloned to your local machine.
+- It generates test files based on the relative paths provided in the `test_files` list.
+- When a file name contains dashes, the corresponding module name will be in camel case (e.g., "Map-Search-Filters.js" -> "MapSearchFilters").
\ No newline at end of file
diff --git a/test/scripts/generate-tests.py b/test/scripts/generate-tests.py
new file mode 100644
index 000000000..3612949c5
--- /dev/null
+++ b/test/scripts/generate-tests.py
@@ -0,0 +1,73 @@
+import os
+import re
+
+# Update these paths to those that you would like to create test files for.
+test_files = [
+ "collections/maps/Geohashes.js",
+ "models/connectors/Filters-Map.js",
+ "models/connectors/Filters-Search.js",
+ "models/connectors/Map-Search-Filters.js",
+ "models/connectors/Map-Search.js",
+ "models/filters/SpatialFilter.js",
+ "models/maps/Geohash.js",
+ "models/maps/assets/CesiumGeohash.js",
+]
+
+test_template = """
+define([
+ "{import_path}",
+], function ({module_name}) {{
+ // Configure the Chai assertion library
+ var should = chai.should();
+ var expect = chai.expect;
+
+ describe("{module_name} Test Suite", function () {{
+ /* Set up */
+ beforeEach(function () {{}});
+
+ /* Tear down */
+ afterEach(function () {{}});
+
+ describe("Initialization", function () {{
+ it("should create a {module_name} instance", function () {{
+ new {module_name}().should.be.instanceof({module_name});
+ }});
+ }});
+ }});
+}});
+"""
+
+def camel_case(name):
+ return "".join(word.capitalize() for word in name.split("-"))
+
+script_dir = os.path.dirname(os.path.realpath(__file__))
+project_root = os.path.join(script_dir, '..', '..')
+
+created_paths = []
+
+for test_file in test_files:
+ test_path = os.path.join(project_root, 'test', 'js', 'specs', 'unit', test_file.replace(".js", ".spec.js"))
+ dir_path = os.path.dirname(test_path)
+
+ if not os.path.exists(dir_path):
+ os.makedirs(dir_path)
+
+ module_path = os.path.join("src", "js", test_file)
+ module_name = camel_case(test_file.split("/")[-1].replace(".js", ""))
+
+ relative_module_path = "../../../../../../../../" + module_path
+ relative_module_path = re.sub(r"\.js$", "", relative_module_path)
+
+ with open(test_path, "w") as f:
+ content = test_template.format(
+ import_path=relative_module_path, module_name=module_name
+ )
+ f.write(content.strip())
+
+ created_paths.append(f'./{os.path.relpath(test_path, project_root)}')
+
+print("Unit test starter files created.")
+print("\nRemember to add these paths to the `test/config/tests.json` file! đđ")
+for path in created_paths:
+ path = re.sub(r"^\./test", ".", path)
+ print(f'"{path}",')