Source: src/js/views/maps/MapView.js

'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.model.get('clickFeatureAction') === 'showDetails'
            ) {
              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 {
            const view = this;
            const interactions = view.model.get('interactions')
            const features = view.model.getSelectedFeatures();

            view.featureInfo = new FeatureInfoView({
              el: view.subElements.featureInfoContainer,
              model: features.at(0)
            }).render()

            // When the selectedFeatures collection changes, update the feature
            // info view
            view.stopListening(features, 'update')
            view.listenTo(features, 'update', function () {
              view.featureInfo.changeModel(features.at(-1))
            })

            // If the Feature model is ever completely replaced for any reason,
            // make the the Feature Info view gets updated.
            const event = 'change:selectedFeatures'
            view.stopListening(interactions, event)
            view.listenTo(interactions, event, view.renderFeatureInfo);
            return view.featureInfo
          }
          catch (e) {
            console.log('Error rendering a FeatureInfoView in a MapView', e);
          }
        },

        /**
         * 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 () {
          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.
          for (const layers of this.model.getLayerGroups()) {
            this.stopListening(layers);
            this.listenTo(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;
        },

        /**
         * 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 {
            const interactions = this.model.get('interactions')
            if (!interactions) {
              this.listenToOnce(this.model, 'change:interactions', this.renderScaleBar);
              return
            }
            this.scaleBar = new ScaleBarView({
              el: this.subElements.scaleBarContainer,
              scaleModel: interactions.get('scale'),
              pointModel: interactions.get('mousePosition')
            })
            this.scaleBar.render();

            // If the interaction model or relevant sub-models are ever completely
            // replaced for any reason, re-render the scale bar.
            this.listenToOnce(interactions, 'change:scale change:mousePosition', this.renderScaleBar);
            this.listenToOnce(this.model, 'change:interactions', this.renderScaleBar);

            return this.scaleBar;
          }
          catch (error) {
            console.log(
              'There was an error rendering a ScaleBarView in a MapView' +
              '. Error details: ' + error
            );
          }
        },

        /**
         * Get a list of the views that this view contains.
         * @returns {Backbone.View[]} Returns an array of all of the sub-views.
         * Some may be undefined if they have not been rendered yet.
         * @since 2.27.0
         */
        getSubViews: function () {
          return [
            this.mapWidget,
            this.toolbar,
            this.featureInfo,
            this.layerDetails,
            this.scaleBar
          ]
        },

        /**
         * Executed when the view is closed. This will close all of the sub-views.
         * @since 2.27.0
         */
        onClose: function () {
          const subViews = this.getSubViews()
          subViews.forEach(subView => {
            if (subView && typeof subView.onClose === 'function') {
              subView.onClose()
            }
          })
        }

      }
    );

    return MapView;

  }
);