define(["jquery",
"underscore",
"backbone",
"gmaps",
"collections/Filters",
"models/filters/FilterGroup",
"models/filters/SpatialFilter",
"models/Stats",
"views/DataCatalogView",
"views/filters/FilterGroupsView",
"text!templates/dataCatalog.html",
"nGeohash"
],
function($, _, Backbone, gmaps, Filters, 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.
* @classcategory Views
* @extends DataCatalogView
* @constructor
*/
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),
/**
* 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 contructing 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();
}
// 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 filter groups and renders them in this view
*/
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
});
//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",
"documents"];
// 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("<div id='map-canvas'></div>");
this.map = new gmaps.Map($("#map-canvas")[0], mapOptions);
this.mapModel.set("map", this.map);
this.hasZoomed = false;
this.hasDragged = false;
// Hide the map filter toggle element
this.$(this.mapFilterToggle).hide();
// 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();
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);
//Mark the view as ready to start a search
viewRef.ready = true;
// Trigger a new search
viewRef.triggerSearch();
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, "dragend", function() {
viewRef.hasDragged = true;
});
}
});
return DataCatalogViewWithFilters;
});