# Source: src/js/models/maps/assets/CesiumVectorData.js

    'use strict';

    define(
      [
        'jquery',
        'underscore',
        'backbone',
        'cesium',
        'models/maps/assets/MapAsset',
        'models/maps/AssetColor',
        'models/maps/AssetColorPalette',
        'collections/maps/VectorFilters'
      ],
      function (
        $,
        _,
        Backbone,
        Cesium,
        MapAsset,
        AssetColor,
        AssetColorPalette,
        VectorFilters
      ) {
        /**
         * @classdesc A CesiumVectorData Model is a vector layer (excluding Cesium3DTilesets)
         * that can be used in Cesium maps. This model corresponds to "DataSource" models in
         * Cesium. For example, this could represent vectors rendered from a Cesium
         * GeoJSONDataSource.
         * {@link https://cesium.com/learn/cesiumjs/ref-doc/GeoJsonDataSource.html}. Note:
         * GeoJsonDataSource is the only supported DataSource so far, eventually this model
         * could be used to support Cesium's CzmlDataSource and KmlDataSource (and perhaps a
         * Cesium CustomDataSource).
         * @classcategory Models/Maps/Assets
         * @class CesiumVectorData
         * @name CesiumVectorData
         * @extends MapAsset
         * @since 2.19.0
         * @constructor
        */
        var CesiumVectorData = MapAsset.extend(
          /** @lends CesiumVectorData.prototype */ {

            /**
             * The name of this type of model
             * @type {string}
            */
            type: 'CesiumVectorData',

            /**
             * Options that are supported for creating Cesium DataSources. The object will be
             * passed to the cesium DataSource's load method as options, so the properties
             * listed in the Cesium documentation are also supported. Each type of Cesium Data
             * Source has a specific set of load method options. See for example, the
             * GeoJsonDataSource options:
             * {@link https://cesium.com/learn/cesiumjs/ref-doc/GeoJsonDataSource.html}
             * @typedef {Object} CesiumVectorData#cesiumOptions
             * @property {string|Object} data - The url, GeoJSON object, or TopoJSON object to
             * be loaded.
             */

            /**
             * Default attributes for CesiumVectorData models
             * @name CesiumVectorData#defaults
             * @extends MapAsset#defaults
             * @type {Object}
             * @property {'GeoJsonDataSource'} type The format of the data. Must be
             * 'GeoJsonDataSource'. (The only Cesium DataSource supported so far.)
             * @property {VectorFilters} [filters=new VectorFilters()] A set of conditions
             * used to show or hide specific features of this vector data.
             * @property {AssetColorPalette} [colorPalette=new AssetColorPalette()] The color
             * or colors mapped to attributes of this asset. Used to style the features and to
             * make a legend.
             * @property {Cesium.GeoJsonDataSource} cesiumModel A Cesium DataSource model
             * created and used by Cesium that organizes the data to display in the Cesium
             * Widget. See
             * {@link https://cesium.com/learn/cesiumjs/ref-doc/DataSource.html?classFilter=DataSource}
             * @property {CesiumVectorData#cesiumOptions} cesiumOptions options are passed to
             * the function that creates the Cesium model. The properties of options are
             * specific to each type of asset.
            */
            defaults: function () {
              return _.extend(
                this.constructor.__super__.defaults(),
                {
                  type: 'GeoJsonDataSource',
                  filters: new VectorFilters(),
                  cesiumModel: null,
                  cesiumOptions: {},
                  colorPalette: new AssetColorPalette(),
                  icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M384 352h-1l-39-65a64 64 0 0 0 0-62l39-65h1a64 64 0 1 0-55-96H119a64 64 0 1 0-87 87v210a64 64 0 1 0 87 87h210a64 64 0 0 0 119-32c0-35-29-64-64-64zm-288 9V151a64 64 0 0 0 23-23h208l-38 64h-1a64 64 0 1 0 0 128h1l38 64H119a64 64 0 0 0-23-23zm176-105a16 16 0 1 1 32 0 16 16 0 0 1-32 0zM400 96a16 16 0 1 1-32 0 16 16 0 0 1 32 0zM64 80a16 16 0 1 1 0 32 16 16 0 0 1 0-32zM48 416a16 16 0 1 1 32 0 16 16 0 0 1-32 0zm336 16a16 16 0 1 1 0-32 16 16 0 0 1 0 32z"/></svg>'
                }
              );
            },

            /**
             * Executed when a new CesiumVectorData model is created.
             * @param {MapConfig#MapAssetConfig} [assetConfig] The initial values of the
             * attributes, which will be set on the model.
             */
            initialize: function (assetConfig) {
              try {

                MapAsset.prototype.initialize.call(this, assetConfig);

                if (assetConfig.filters) {
                  this.set('filters', new VectorFilters(assetConfig.filters))
                }

                // displayReady will be updated by the Cesium map within which the asset is
                // rendered. The map will set it to true when the data is ready to be
                // rendered. Used to know when it's safe to calculate a bounding sphere.
                this.set('displayReady', false)

                this.createCesiumModel();
              }
              catch (error) {
                console.log(
                  'There was an error initializing a CesiumVectorData model' +
                  '. Error details: ' + error
                );
              }
            },

            /**
             * Creates a Cesium.DataSource model and sets it to this model's
             * 'cesiumModel' attribute. This cesiumModel contains all the information required
             * for Cesium to render the vector data. See
             * {@link https://cesium.com/learn/cesiumjs/ref-doc/DataSource.html?classFilter=DataSource}
             * @param {Boolean} recreate - Set recreate to true to force the function create
             * the Cesium Model again. Otherwise, if a cesium model already exists, that is
             * returned instead.
             */
            createCesiumModel: function (recreate = false) {

              try {

                const model = this;
                const cesiumOptions = model.get('cesiumOptions')
                const type = model.get('type')
                const label = model.get('label') || ''
                const dataSourceFunction = Cesium[type]

                // If the cesium model already exists, don't create it again unless specified
                if (!recreate && model.get('cesiumModel')) {
                  return model.get('cesiumModel')
                }

                model.resetStatus();

                if (!cesiumOptions || !cesiumOptions.data) {
                  model.set('status', 'error');
                  model.set('statusDetails', 'Vector data source is missing: A URL or GeoJSON/TopoJson object is required')
                  return
                }

                if (dataSourceFunction && typeof dataSourceFunction === 'function') {
                  let dataSource = new dataSourceFunction(label)

                  const data = cesiumOptions.data;
                  delete cesiumOptions.data

                  dataSource.load(data, cesiumOptions)
                    .then(function (loadedData) {
                      model.set('cesiumModel', loadedData)
                      model.setListeners()
                      model.updateFeatureVisibility()
                      model.updateAppearance()
                      model.set('status', 'ready')
                    })
                    .otherwise(function (error) {
                      // See https://cesium.com/learn/cesiumjs/ref-doc/RequestErrorEvent.html
                      let details = error;
                      // Write a helpful error message
                      switch (error.statusCode) {
                        case 404:
                          details = 'The resource was not found (error code 404).'
                          break;
                        case 500:
                          details = 'There was a server error (error code 500).'
                          break;
                      }
                      model.set('status', 'error');
                      model.set('statusDetails', details)
                    })
                } else {
                  model.set('status', 'error')
                  model.set('statusDetails', type + ' is not a supported imagery type.')
                }
              }
              catch (error) {
                console.log(
                  'Failed to create a Cesium Model for a CesiumVectorData model' +
                  '. Error details: ' + error
                );
              }
            },

            /**
             * Set listeners that update the cesium model when the backbone model is updated.
             */
            setListeners: function () {
              try {
                this.stopListening(this, 'change:visible change:opacity change:color')
                this.listenTo(
                  this, 'change:visible change:opacity change:color', this.updateAppearance
                )
                this.stopListening(this.get('filters'), 'update')
                this.listenTo(this.get('filters'), 'update', this.updateFeatureVisibility)
              }
              catch (error) {
                console.log(
                  'There was an error setting listeners in a CesiumVectorData model' +
                  '. Error details: ' + error
                );
              }
            },

            /**
             * Checks that the map is ready to display this asset. The displayReady attribute
             * is updated by the Cesium map when the dataSourceDisplay is updated.
             * @returns {Promise} Returns a promise that resolves to this model when ready to
             * be displayed.
            */
            whenDisplayReady: function () {
              return this.whenReady()
                .then(function (model) {
                  return new Promise(function (resolve, reject) {
                    if (model.get('displayReady')) {
                      resolve(model)
                      return
                    }
                    model.stopListening(model, 'change:displayReady')
                    model.listenTo(model, 'change:displayReady', function () {
                      if (model.get('displayReady')) {
                        model.stopListening(model, 'change:displayReady')
                        resolve(model)
                      }
                    })
                  });
                })
            },

            /**
             * Given a feature from a Cesium Vector Data source, returns any properties that are set
             * on the feature, similar to an attributes table.
             * @param {Cesium.Entity} feature A Cesium Entity
             * @returns {Object} An object containing key-value mapping of property names to
             * properties.
            */
            getPropertiesFromFeature(feature) {
              try {
                const featureProps = feature.properties
                let properties = {}
                if (featureProps) {
                  properties = feature.properties.getValue(new Date())
                }
                properties = this.addCustomProperties(properties)
                return properties
              }
              catch (error) {
                console.log(
                  'There was an error getting properties from a Cesium Entity' +
                  '. Error details: ' + error +
                  '. Returning an empty object.'
                );
                return {}
              }
            },

            /**
             * Updates the styles set on the cesiumModel object based on the colorPalette and
             * filters attributes.
             */
            updateAppearance: function () {
              try {

                const model = this;
                const cesiumModel = this.get('cesiumModel')
                const opacity = this.get('opacity')
                const entities = cesiumModel.entities.values

                // Suspending events while updating a large number of entities helps
                // performance.
                cesiumModel.entities.suspendEvents()

                // If the asset isn't visible at all, don't bother setting up colors. Just set
                // every feature to hidden.
                if (!model.isVisible()) {
                  cesiumModel.entities.show = false
                  // Indicate that the layer is hidden if the opacity is zero by updating the
                  // visibility property
                  if (model.get('opacity') === 0) {
                    model.set('visible', false);
                  }
                } else {
                  cesiumModel.entities.show = true
                  for (var i = 0; i < entities.length; i++) {

                    const entity = entities[i];
                    const properties = model.getPropertiesFromFeature(entity)

                    let outlineColor = null
                    let featureOpacity = opacity
                    let outline = false
                    // For polylines
                    let lineWidth = 3
                    // For billboard pins and points. We could make size configurable. Size
                    // could also be set according to a vector property
                    let markerSize = 25

                    // If the feature is selected, set the opacity to 1, and add an outline
                    if (model.featureIsSelected(entity)) {
                      featureOpacity = 1
                      outline = true
                      // TODO: This colour should be configurable in the Map model
                      outlineColor = Cesium.Color.WHITE
                      lineWidth = 7
                      markerSize = 34
                    }

                    const rgb = model.getColor(properties)
                    const color = new Cesium.Color(
                      rgb.red, rgb.green, rgb.blue, featureOpacity
                    )

                    if (entity.polygon) {
                      entity.polygon.material = color
                      entity.polygon.outline = outline;
                      entity.polygon.outlineColor = outlineColor
                      entity.polygon.outlineWidth = outline ? 2 : 0
                    }
                    if (entity.billboard) {
                      if (!model.pinBuilder) {
                        model.pinBuilder = new Cesium.PinBuilder()
                      }
                      entity.billboard.image = model.pinBuilder.fromColor(color, markerSize).toDataURL()
                      // To convert the automatically created billboards to points instead:
                      // entity.billboard = undefined;
                      // entity.point = new Cesium.PointGraphics();
                    }
                    if (entity.point) {
                      entity.point.color = color
                      entity.point.outlineColor = outlineColor
                      entity.point.outlineWidth = outline ? 2 : 0
                      // Points look better a little smaller than billboards
                      entity.point.pixelSize = (markerSize * 0.5);
                    }
                    if (entity.polyline) {
                      entity.polyline.material = color
                      entity.polyline.width = lineWidth
                    }

                  }
                }

                cesiumModel.entities.resumeEvents()

                // Let the map and/or other parent views know that a change has been made that
                // requires the map to be re-rendered
                model.trigger('appearanceChanged')

              }
              catch (error) {
                console.log(
                  'There was an error updating CesiumVectorData model styles' +
                  '. Error details: ' + error
                );
              }
            },

            /**
             * Shows or hides each feature from this Map Asset based on the filters.
             */
            updateFeatureVisibility: function () {
              try {
                const model = this;
                const cesiumModel = model.get('cesiumModel')
                const entities = cesiumModel.entities.values
                const filters = model.get('filters')

                // Suspending events while updating a large number of entities helps
                // performance.
                cesiumModel.entities.suspendEvents()

                for (var i = 0; i < entities.length; i++) {
                  let visible = true
                  const entity = entities[i]
                  if (filters && filters.length) {
                    const properties = model.getPropertiesFromFeature(entity)
                    visible = model.featureIsVisible(properties)
                  }
                  entity.show = visible
                }

                cesiumModel.entities.resumeEvents()

                // Let the map and/or other parent views know that a change has been made that
                // requires the map to be re-rendered
                model.trigger('appearanceChanged')
              }
              catch (error) {
                console.log(
                  'There was an error updating CesiumVectorData feature visibility' +
                  '. Error details: ' + error
                );
              }
            },

            /**
            * Waits for the model to be ready to display, then gets a Cesium Bounding Sphere
            * that can be used to navigate to view the full extent of the vector data. See
            * {@link https://cesium.com/learn/cesiumjs/ref-doc/BoundingSphere.html}.
            * @param {Cesium.DataSourceDisplay} dataSourceDisplay The data source display
            * attached to the CesiumWidget scene that this bounding sphere is for. Required.
            * @returns {Promise} Returns a promise that resolves to a Cesium Bounding Sphere
            * when ready
            */
            getBoundingSphere: function (dataSourceDisplay) {
              return this.whenDisplayReady()
                .then(function (model) {
                  const entities = model.get('cesiumModel').entities.values.slice(0)
                  const boundingSpheres = [];
                  const boundingSphereScratch = new Cesium.BoundingSphere();
                  for (let i = 0, len = entities.length; i < len; i++) {
                    let state = Cesium.BoundingSphereState.PENDING;
                    state = dataSourceDisplay.getBoundingSphere(
                      entities[i], false, boundingSphereScratch
                    )
                    if (state === Cesium.BoundingSphereState.PENDING) {
                      return false;
                    } else if (state !== Cesium.BoundingSphereState.FAILED) {
                      boundingSpheres.push(Cesium.BoundingSphere.clone(boundingSphereScratch));
                    }
                  }
                  if (boundingSpheres.length) {
                    return Cesium.BoundingSphere.fromBoundingSpheres(boundingSpheres);
                  }
                  return false
                }).catch(function (error) {
                  console.log(
                    'Failed to get the bounding sphere for a CesiumVectorData model' +
                    '. Error details: ' + error
                  );
                })
            },

          });

        return CesiumVectorData;

      }
    );