'use strict';
define(
[
'jquery',
'underscore',
'backbone',
'models/maps/Map',
'text!templates/maps/map.html',
// SubViews
'views/maps/CesiumWidgetView',
'views/maps/ToolbarView',
'views/maps/ScaleBarView',
'views/maps/FeatureInfoView',
'views/maps/LayerDetailsView',
// CSS
'text!' + MetacatUI.root + '/css/map-view.css',
],
function (
$,
_,
Backbone,
Map,
Template,
// SubViews
CesiumWidgetView,
ToolbarView,
ScaleBarView,
FeatureInfoView,
LayerDetailsView,
// CSS
MapCSS
) {
/**
* @class MapView
* @classdesc An interactive 2D or 3D map that allows visualization of geo-spatial
* data.
* @classcategory Views/Maps
* @name MapView
* @extends Backbone.View
* @screenshot views/maps/MapView.png
* @since 2.18.0
* @constructs
*/
var MapView = Backbone.View.extend(
/** @lends MapView.prototype */{
/**
* The type of View this is
* @type {string}
*/
type: 'MapView',
/**
* The HTML classes to use for this view's element
* @type {string}
*/
className: 'map-view',
/**
* The model that this view uses
* @type {Map}
*/
model: null,
/**
* The primary HTML template for this view
* @type {Underscore.template}
*/
template: _.template(Template),
/**
* Classes that will be used to select specific elements from the template.
* @name MapView#classes
* @type {Object}
* @property {string} mapWidgetContainer The element that will hold the map widget
* (i.e. CesiumWidgetView)
* @property {string} scaleBarContainer The container for the ScaleBarView
* @property {string} featureInfoContainer The container for the box that will
* show details about a selected feature
* @property {string} toolbarContainer The container for the toolbar UI
* @property {string} layerDetailsContainer The container for the element that
* will show details about a specific layer
*/
classes: {
mapWidgetContainer: 'map-view__map-widget-container',
scaleBarContainer: 'map-view__scale-bar-container',
featureInfoContainer: 'map-view__feature-info-container',
toolbarContainer: 'map-view__toolbar-container',
layerDetailsContainer: 'map-view__layer-details-container'
},
/**
* The events this view will listen to and the associated function to call.
* @type {Object}
*/
events: {
// 'event selector': 'function',
},
/**
* Executed when a new MapView is created
* @param {Object} [options] - A literal object with options to pass to the view.
*/
initialize: function (options) {
try {
// Add the CSS required for this view and its sub-views.
MetacatUI.appModel.addCSS(MapCSS, 'mapView');
// Get all the options and apply them to this view
if (typeof options == 'object') {
for (const [key, value] of Object.entries(options)) {
this[key] = value;
}
}
if(!this.model) {
this.model = new Map();
}
} catch (e) {
console.log('A MapView failed to initialize. Error message: ' + e);
}
},
/**
* Renders this view
* @return {MapView} Returns the rendered view element
*/
render: function () {
try {
// Save a reference to this view
var view = this;
// TODO: Add a nice loading animation?
// Insert the template into the view
this.$el.html(this.template());
// Ensure the view's main element has the given class name
this.el.classList.add(this.className);
// Select the elements that will be updatable
this.subElements = {};
for (const [element, className] of Object.entries(view.classes)) {
view.subElements[element] = document.querySelector('.' + className)
}
// Render the (Cesium) map
this.renderMapWidget();
// Optionally add the toolbar, layer details, scale bar, and feature info box.
if (this.model.get('showToolbar')) {
this.renderToolbar();
this.renderLayerDetails();
}
if (this.model.get('showScaleBar')) {
this.renderScaleBar();
}
if (this.model.get('showFeatureInfo')) {
this.renderFeatureInfo();
}
// Return this MapView
return this
}
catch (error) {
console.log(
'There was an error rendering a MapView' +
'. Error details: ' + error
);
}
},
/**
* Renders the view that shows the map/globe and all of the geo-spatial data.
* Currently, this uses the CesiumWidgetView, but this function could be modified
* to use an alternative map widget in the future.
* @returns {CesiumWidgetView} Returns the rendered view
*/
renderMapWidget: function () {
try {
this.mapWidget = new CesiumWidgetView({
el: this.subElements.mapWidgetContainer,
model: this.model
})
this.mapWidget.render()
return this.mapWidget
}
catch (error) {
console.log(
'There was an error rendering the map widget in a MapView' +
'. Error details: ' + error
);
}
},
/**
* Renders the toolbar element that contains sections for viewing and editing the
* layer list.
* @returns {ToolbarView} Returns the rendered view
*/
renderToolbar: function () {
try {
this.toolbar = new ToolbarView({
el: this.subElements.toolbarContainer,
model: this.model
})
this.toolbar.render()
return this.toolbar
}
catch (error) {
console.log(
'There was an error rendering a toolbarView in a MapView' +
'. Error details: ' + error
);
}
},
/**
* Renders the info box that is displayed when a user clicks on a feature on the
* map. If there are multiple features selected, this will show information for
* the first one only.
* @returns {FeatureInfoView} Returns the rendered view
*/
renderFeatureInfo: function () {
try {
this.featureInfo = new FeatureInfoView({
el: this.subElements.featureInfoContainer,
model: this.model.get('selectedFeatures').at(0)
})
this.featureInfo.render()
// When the selectedFeatures collection changes, update the feature info view
function setSelectFeaturesListeners() {
this.stopListening(this.model.get('selectedFeatures'), 'update')
this.listenTo(this.model.get('selectedFeatures'), 'update', function () {
this.featureInfo.changeModel(this.model.get('selectedFeatures').at(-1))
})
}
setSelectFeaturesListeners.call(this)
// If the Feature model is ever completely replaced for any reason, make the
// the Feature Info view gets updated.
this.stopListening(this.model, 'change:selectedFeatures')
this.listenTo(this.model, 'change:selectedFeatures', function (mapModel, featuresCollection) {
this.featureInfo.changeModel(featuresCollection.at(-1))
setSelectFeaturesListeners.call(this)
})
return this.featureInfo
}
catch (error) {
console.log(
'There was an error rendering a FeatureInfoView in a MapView' +
'. Error details: ' + error
);
}
},
/**
* Renders the layer details view that is displayed when a user clicks on a layer
* in the toolbar.
* @returns {LayerDetailsView} Returns the rendered view
*/
renderLayerDetails: function () {
try {
this.layerDetails = new LayerDetailsView({
el: this.subElements.layerDetailsContainer
})
this.layerDetails.render()
// When a layer is selected, show the layer details panel. When a layer is
// de-selected, close it. The Layer model's 'selected' attribute gets updated
// from the Layer Item View, and also from the Layers collection.
this.stopListening(this.model.get('layers'))
this.listenTo(this.model.get('layers'), 'change:selected',
function (layerModel, selected) {
if (selected === false) {
this.layerDetails.updateModel(null)
this.layerDetails.close()
} else {
this.layerDetails.updateModel(layerModel)
this.layerDetails.open()
}
}
)
return this.layerDetails
}
catch (error) {
console.log(
'There was an error rendering a LayerDetailsView in a MapView' +
'. Error details: ' + error
);
}
},
/**
* Renders the scale bar view that shows the current position of the mouse on the
* map.
* @returns {ScaleBarView} Returns the rendered view
*/
renderScaleBar: function () {
try {
this.scaleBar = new ScaleBarView({
el: this.subElements.scaleBarContainer
})
this.scaleBar.render()
this.stopListening(this.model, 'change:currentPosition')
this.listenTo(this.model, 'change:currentPosition', function (model, position) {
this.scaleBar.updateCoordinates(position.latitude, position.longitude)
})
this.stopListening(this.model, 'change:currentScale')
this.listenTo(this.model, 'change:currentScale', function (model, scale) {
this.scaleBar.updateScale(scale.pixels, scale.meters)
})
return this.scaleBar
}
catch (error) {
console.log(
'There was an error rendering a ScaleBarView in a MapView' +
'. Error details: ' + error
);
}
},
}
);
return MapView;
}
);