diff --git a/src/css/map-view.css b/src/css/map-view.css
index ce9bddef6..d3c9439e8 100644
--- a/src/css/map-view.css
+++ b/src/css/map-view.css
@@ -120,6 +120,10 @@
font-size: 1rem;
}
+.map-view__button--active {
+ background-color: var(--map-col-highlight);
+}
+
/* ---- BADGE ---- */
.map-view__badge{
@@ -953,4 +957,18 @@ other class: .ui-slider-range */
box-shadow: var(--map-shadow-md);
/* imagery appears lighter on the map */
filter: brightness(1.75);
-}
\ No newline at end of file
+}
+
+/*****************************************************************************************
+ *
+ * Draw Tool
+ *
+ * Panel for drawing polygons in the map
+ *
+ */
+
+.draw-tool {
+ display: grid;
+ grid-auto-rows: min-content;
+ grid-gap: 1rem;
+}
diff --git a/src/js/collections/maps/Features.js b/src/js/collections/maps/Features.js
index 768c17197..c49eb33b3 100644
--- a/src/js/collections/maps/Features.js
+++ b/src/js/collections/maps/Features.js
@@ -1,125 +1,118 @@
-'use strict';
+"use strict";
-define(
- [
- 'jquery',
- 'underscore',
- 'backbone',
- 'models/maps/Feature'
- ],
- function (
- $,
- _,
- Backbone,
- Feature
- ) {
+define(["jquery", "underscore", "backbone", "models/maps/Feature"], function (
+ $,
+ _,
+ Backbone,
+ Feature
+) {
+ /**
+ * @class Features
+ * @classdesc A Features collection contains the relevant properties of a group of
+ * selected geo-spatial features from a map.
+ * @class Features
+ * @classcategory Collections/Maps
+ * @extends Backbone.Collection
+ * @since 2.18.0
+ * @constructor
+ */
+ var Features = Backbone.Collection.extend(
+ /** @lends Features.prototype */ {
+ /**
+ * The class/model that this collection contains.
+ * @type {Backbone.Model}
+ */
+ model: Feature,
- /**
- * @class Features
- * @classdesc A Features collection contains the relevant properties of a group of
- * selected geo-spatial features from a map.
- * @class Features
- * @classcategory Collections/Maps
- * @extends Backbone.Collection
- * @since 2.18.0
- * @constructor
- */
- var Features = Backbone.Collection.extend(
- /** @lends Features.prototype */ {
+ /**
+ * Get an array of all of the unique Map Assets that are associated with this
+ * collection. (When a feature model is part of a layer, it will have the layer
+ * model (Map Asset) set as a property)
+ * @returns {MapAsset[]} Returns an a array of all the unique Map Assets (imagery,
+ * tile sets, etc.) in this collection.
+ */
+ getMapAssets: function () {
+ return this.getUniqueAttrs("mapAsset");
+ },
- /**
- * The class/model that this collection contains.
- * @type {Backbone.Model}
- */
- model: Feature,
+ /**
+ * Get an array of all the unique feature objects associated with this collection.
+ * @param {string} [type] Optionally set a type of feature to return. If set, then
+ * only features that have this constructor name will be returned.
+ * @returns {Array} Returns an array of all of the unique feature objects in the
+ * collection. Feature objects are the objects used by the map widget to represent
+ * a feature in the map. For example, in Cesium this could be a
+ * Cesium3DTileFeature or an Entity.
+ */
+ getFeatureObjects: function (type) {
+ let featureObjects = this.getUniqueAttrs("featureObject");
+ if (type) {
+ featureObjects = featureObjects.filter(function (featureObject) {
+ return featureObject.constructor.name === type;
+ });
+ }
+ return featureObjects;
+ },
- /**
- * Get an array of all of the unique Map Assets that are associated with this
- * collection. (When a feature model is part of a layer, it will have the layer
- * model (Map Asset) set as a property)
- * @returns {MapAsset[]} Returns an a array of all the unique Map Assets (imagery,
- * tile sets, etc.) in this collection.
- */
- getMapAssets: function () {
- return this.getUniqueAttrs('mapAsset')
- },
+ /**
+ * Get an array of unique values for some attribute that may be set on the models
+ * in this collection
+ * @param {string} attrName The name of the attr to get unique values for
+ * @returns {Array} Returns an array of unique values of the given attribute
+ */
+ getUniqueAttrs: function (attrName) {
+ try {
+ let uniqueAttrs = [];
+ this.each(function (featureModel) {
+ const attr = featureModel.get(attrName);
+ if (attr && !uniqueAttrs.includes(attr)) {
+ uniqueAttrs.push(attr);
+ }
+ });
+ return uniqueAttrs;
+ } catch (error) {
+ console.log(
+ `Failed to get unique attributes for "${attrName}".`,
+ error
+ );
+ }
+ },
- /**
- * Get an array of all the unique feature objects associated with this collection.
- * @param {string} [type] Optionally set a type of feature to return. If set, then
- * only features that have this constructor name will be returned.
- * @returns {Array} Returns an array of all of the unique feature objects in the
- * collection. Feature objects are the objects used by the map widget to represent
- * a feature in the map. For example, in Cesium this could be a
- * Cesium3DTileFeature or an Entity.
- */
- getFeatureObjects: function (type) {
- let featureObjects = this.getUniqueAttrs('featureObject')
- if (type) {
- featureObjects = featureObjects.filter(function (featureObject) {
- return featureObject.constructor.name === type
- })
- }
- return featureObjects
- },
+ /**
+ * Checks if a given feature object is an attribute in one of the Feature models
+ * in this collection.
+ * @param {Feature|Cesium.Cesium3DTilesetFeature|Cesium.Entity} featureObject
+ * @returns {boolean} Returns true if the given feature object is in this
+ * collection, false otherwise.
+ * @since 2.25.0
+ */
+ containsFeature: function (featureObject) {
+ if (this.models.length === 0) return false;
+ if (!featureObject) return false;
+ featureObject =
+ featureObject instanceof Feature
+ ? featureObject.get("featureObject")
+ : featureObject;
+ return this.findWhere({ 'featureObject': featureObject }) ? true : false;
+ },
- /**
- * Get an array of unique values for some attribute that may be set on the models
- * in this collection
- * @param {string} attrName The name of the attr to get unique values for
- * @returns {Array} Returns an array of unique values of the given attribute
- */
- getUniqueAttrs: function (attrName) {
- try {
- let uniqueAttrs = []
- this.each(function (featureModel) {
- const attr = featureModel.get(attrName)
- if (attr && !uniqueAttrs.includes(attr)) {
- uniqueAttrs.push(attr)
- }
- })
- return uniqueAttrs
- }
- catch (error) {
- console.log(
- 'Failed to get unique values for an attribute in a Features collection' +
- '. Error details: ' + error
- );
- }
- },
+ /**
+ * Checks if a given array of feature objects are attributes in one of the
+ * Feature models in this collection.
+ * @param {Array} featureObjects An array of feature objects to check if they are
+ * in this collection.
+ * @returns {boolean} Returns true if all of the given feature objects are in this
+ * collection, false otherwise.
+ */
+ containsFeatures: function (featureObjects) {
+ if (!featureObjects || !featureObjects.length) return false;
+ return featureObjects.every((featureObject) =>
+ this.containsFeature(featureObject)
+ );
+ },
- /**
- * Checks if a given feature object is an attribute in one of the Feature models
- * in this collection.
- * @param {Feature|Cesium.Cesium3DTilesetFeature|Cesium.Entity} featureObject
- * @returns {boolean} Returns true if the given feature object is in this
- * collection, false otherwise.
- * @since 2.25.0
- */
- containsFeature: function (featureObject) {
- if (!featureObject) return false;
- featureObject = featureObject instanceof Feature ? featureObject.get('featureObject') : featureObject;
- return this.findWhere({ featureObject: featureObject }) ? true : false;
- },
+ }
+ );
- /**
- * Checks if a given array of feature objects are attributes in one of the
- * Feature models in this collection.
- * @param {Array} featureObjects An array of feature objects to check if they are
- * in this collection.
- * @returns {boolean} Returns true if all of the given feature objects are in this
- * collection, false otherwise.
- */
- containsFeatures: function (featureObjects) {
- if (!featureObjects || !featureObjects.length) return false;
- return featureObjects.every(
- (featureObject) => this.containsFeature(featureObject));
- },
-
- }
- );
-
- return Features;
-
- }
-);
\ No newline at end of file
+ return Features;
+});
diff --git a/src/js/collections/maps/GeoPoints.js b/src/js/collections/maps/GeoPoints.js
new file mode 100644
index 000000000..566a33ef2
--- /dev/null
+++ b/src/js/collections/maps/GeoPoints.js
@@ -0,0 +1,330 @@
+"use strict";
+
+define(["backbone", "models/maps/GeoPoint"], function (Backbone, GeoPoint) {
+ /**
+ * @class GeoPoints
+ * @classdesc A group of ordered geographic points.
+ * @class GeoPoints
+ * @classcategory Collections/Maps
+ * @extends Backbone.Collection
+ * @since x.x.x
+ * @constructor
+ */
+ var GeoPoints = Backbone.Collection.extend(
+ /** @lends GeoPoints.prototype */ {
+ /**
+ * The class/model that this collection contains.
+ * @type {Backbone.Model}
+ */
+ model: GeoPoint,
+
+ /**
+ * Given a point in various formats, format it such that it can be used to
+ * add to this collection.
+ * @param {Array|Object|GeoPoint} point - Accepted formats are:
+ * - An array of the form [longitude, latitude], with an optional third
+ * element for height
+ * - An object with a "longitude" and "latitude" property, and
+ * optionally a "height" property
+ * - A GeoPoint model
+ * @returns {Object|GeoPoint} Returns an object with "longitude" and
+ * "latitude" properties, and optionally a "height" property, or a
+ * GeoPoint model.
+ */
+ formatPoint: function (point) {
+ let attributes = {};
+ if (Array.isArray(point) && point.length > 1) {
+ attributes.longitude = point[0];
+ attributes.latitude = point[1];
+ if (point[2]) {
+ attributes.height = point[2];
+ }
+ } else if (
+ point instanceof GeoPoint ||
+ (point.latitude && point.longitude)
+ ) {
+ attributes = point;
+ }
+ return attributes;
+ },
+
+ /**
+ * Add a point to the collection. Use this rather than the Backbone add
+ * method to allow for different formats of points to be added.
+ * @param {Array|Object|GeoPoint} point - See {@link formatPoint} for
+ * accepted formats.
+ * @returns {GeoPoint} Returns the GeoPoint model that was added.
+ */
+ addPoint: function (point) {
+ point = this.formatPoint(point);
+ return this.add(point);
+ },
+
+ /**
+ * Remove a specific point from the collection. Use this rather than the
+ * Backbone remove method to allow for different formats of points to be
+ * removed.
+ * @param {Array|Object|GeoPoint|Number} indexOrPoint - The index of the
+ * point to remove, or the point itself. See {@link formatPoint} for
+ * accepted formats.
+ * @returns {GeoPoint} Returns the GeoPoint model that was removed.
+ */
+ removePoint(indexOrPoint) {
+ if (typeof indexOrPoint === "number") {
+ return this.removePointByIndex(indexOrPoint);
+ } else if (Array.isArray(indexOrPoint)) {
+ return this.removePointByAttr(indexOrPoint);
+ } else {
+ // try just removing the point
+ return this.remove(indexOrPoint);
+ }
+ },
+
+ /**
+ * Remove a point from the collection based on its attributes.
+ * @param {Array|Object|GeoPoint} point - Any format supported by
+ * {@link formatPoint} is accepted.
+ * @returns {GeoPoint} Returns the GeoPoint model that was removed.
+ */
+ removePointByAttr: function (point) {
+ point = this.formatPoint(point);
+ const model = this.findWhere(point);
+ return this.remove(model);
+ },
+
+ /**
+ * Remove a point from the collection based on its index.
+ * @param {Number} index - The index of the point to remove.
+ * @returns {GeoPoint} Returns the GeoPoint model that was removed.
+ */
+ removePointByIndex: function (index) {
+ if (index < 0 || index >= this.length) {
+ console.warn("Index out of bounds, GeoPoint not removed.");
+ return;
+ }
+ const model = this.at(index);
+ return this.remove(model);
+ },
+
+ /**
+ * Convert the collection to a GeoJSON object. The output can be the
+ * series of points as Point features, the points connected as a
+ * LineString feature, or the points connected and closed as a Polygon.
+ *
+ * Note: For a "Polygon" geometry type, when there's only one point in the
+ * collection, the output will be a "Point". If there are only two points,
+ * the output will be a "LineString", unless `forceAsPolygon` is set to
+ * true.
+ *
+ * @param {String} geometryType - The type of geometry to create. Can be
+ * "Point", "LineString", or "Polygon".
+ * @param {Boolean} [forceAsPolygon=false] - Set to true to enforce the
+ * output as a polygon for the "Polygon" geometry type, regardless of the
+ * number of points in the collection.
+ * @returns {Object} Returns a GeoJSON object of type "Point",
+ * "LineString", or "Polygon".
+ */
+ toGeoJson: function (geometryType, forceAsPolygon = false) {
+ if (!forceAsPolygon && geometryType === "Polygon" && this.length < 3) {
+ geometryType = this.length === 1 ? "Point" : "LineString";
+ }
+ return {
+ type: "FeatureCollection",
+ features: this.toGeoJsonFeatures(geometryType),
+ };
+ },
+
+ // TODO: Move this to a CZML model, use in GeoHash/es
+
+ /**
+ * Get the header object for a CZML document.
+ * @returns {Object} Returns a CZML header object.
+ */
+ getCZMLHeader: function () {
+ return {
+ id: "document",
+ version: "1.0",
+ name: "GeoPoints",
+ };
+ },
+
+ /**
+ * Convert the collection to a CZML document.
+ * @param {String} geometryType - The type of geometry to create.
+ * @param {Boolean} [forceAsPolygon=false] - Set to true to enforce the
+ * output as a polygon for the "Polygon" geometry type, regardless of the
+ * number of points in the collection.
+ * @returns {Object[]} Returns an array of CZML objects.
+ */
+ toCzml: function (geometryType, forceAsPolygon = false) {
+ if (!forceAsPolygon && geometryType === "Polygon" && this.length < 3) {
+ geometryType = this.length === 1 ? "Point" : "LineString";
+ }
+ let czml = [this.getCZMLHeader()];
+ switch (geometryType) {
+ case "Point":
+ czml = czml.concat(this.toCZMLPoints());
+ break;
+ case "LineString":
+ czml.push(this.getCZMLLineString());
+ break;
+ case "Polygon":
+ czml.push(this.getCZMLPolygon());
+ break;
+ default:
+ break;
+ }
+ return czml;
+ },
+
+ /**
+ * Convert the collection to an array of CZML point objects.
+ * @returns {Object[]} Returns an array of CZML point objects.
+ */
+ toCZMLPoints: function () {
+ return this.models.map((model) => {
+ return model.toCZML();
+ })
+ },
+
+ /**
+ * Convert the collection to a CZML polygon object.
+ * @returns {Object} Returns a CZML polygon object.
+ */
+ getCZMLPolygon: function () {
+ const coords = this.toECEFArray();
+ return {
+ id: this.cid,
+ name: "Polygon",
+ polygon: {
+ positions: {
+ cartesian: coords,
+ },
+ },
+ };
+ },
+
+ /**
+ * Convert the collection to a CZML line string object.
+ * @returns {Object} Returns a CZML line string object.
+ */
+ getCZMLLineString: function () {
+ const coords = this.toECEFArray();
+ return {
+ id: this.cid,
+ name: "LineString",
+ polyline: {
+ positions: {
+ cartesian: coords,
+ },
+ },
+ };
+ },
+
+ /**
+ * Convert the collection to a GeoJSON object. The output can be the
+ * series of points as Point features, the points connected as a
+ * LineString feature, or the points connected and closed as a Polygon.
+ * @param {"Point"|"LineString"|"Polygon"} geometryType - The type of
+ * geometry to create.
+ * @returns {Object[]} Returns an array of GeoJSON features.
+ */
+ toGeoJsonFeatures: function (geometryType) {
+ switch (geometryType) {
+ case "Point":
+ return this.toGeoJsonPointFeatures();
+ case "LineString":
+ return [this.toGeoJsonLineStringFeature()];
+ case "Polygon":
+ return [this.toGeoJsonPolygonFeature()];
+ default:
+ return [];
+ }
+ },
+
+ /**
+ * Convert the collection to an array of GeoJSON point features.
+ * @returns {Object[]} Returns an array of GeoJSON point features.
+ */
+ toGeoJsonPointFeatures: function () {
+ return this.models.map((model) => {
+ return model.toGeoJsonFeature();
+ });
+ },
+
+ /**
+ * Convert the collection to a GeoJSON LineString feature.
+ * @returns {Object} Returns a GeoJSON LineString feature.
+ */
+ toGeoJsonLineStringFeature: function () {
+ return {
+ type: "Feature",
+ geometry: {
+ type: "LineString",
+ coordinates: this.to2DArray(),
+ },
+ properties: {},
+ };
+ },
+
+ /**
+ * Convert the collection to a GeoJSON Polygon feature. The polygon will
+ * be closed if it isn't already.
+ * @returns {Object} Returns a GeoJSON Polygon feature.
+ */
+ toGeoJsonPolygonFeature: function () {
+ const coordinates = this.to2DArray();
+ // Make sure the polygon is closed
+ if (coordinates[0] != coordinates[coordinates.length - 1]) {
+ coordinates.push(coordinates[0]);
+ }
+ return {
+ type: "Feature",
+ geometry: {
+ type: "Polygon",
+ coordinates: [coordinates],
+ },
+ properties: {},
+ };
+ },
+
+ /**
+ * Convert the collection to an array of arrays, where each sub-array
+ * contains the longitude and latitude of a point.
+ * @returns {Array[]} Returns an array of arrays.
+ */
+ to2DArray: function () {
+ return this.models.map((model) => {
+ return model.to2DArray();
+ });
+ },
+
+ /**
+ * Convert the collection to a cartesian array, where each every three
+ * elements represents the x, y, and z coordinates of a vertex, e.g.
+ * [x1, y1, z1, x2, y2, z2, ...].
+ * @returns {Array} Returns an array of numbers.
+ */
+ toECEFArray: function () {
+ return this.models.flatMap((model) => {
+ return model.toECEFArray();
+ });
+ },
+
+ /**
+ * Convert the collection to an array of coordinates in the format
+ * native to the map widget. For Cesium, this is an array of
+ * Cartesian3 objects in ECEF coordinates.
+ * @returns {Array} An array of coordinates that can be used by the map
+ * widget.
+ */
+ asMapWidgetCoords: function () {
+ return this.models.map((model) => {
+ return model.get("mapWidgetCoords");
+ });
+ },
+ }
+ );
+
+ return GeoPoints;
+});
diff --git a/src/js/collections/maps/Geohashes.js b/src/js/collections/maps/Geohashes.js
index f606140b1..8c2846f45 100644
--- a/src/js/collections/maps/Geohashes.js
+++ b/src/js/collections/maps/Geohashes.js
@@ -125,21 +125,22 @@ define([
* Creates hashStrings for geohashes that are within the provided bounding
* boxes at the given precision. The returned hashStrings are not
* necessarily in the collection.
- * @param {Object} bounds - Bounding box with north, south, east, and west
+ * @param {GeoBoundingBox} bounds - Bounding box with north, south, east, and west
* properties.
* @param {number} precision - Geohash precision level.
* @returns {string[]} Array of geohash hashStrings.
*/
getHashStringsForBounds: function (bounds, precision) {
this.validatePrecision(precision, false);
- if (!this.boundsAreValid(bounds)) {
+ if (!bounds.isValid()) {
throw new Error("Bounds are invalid");
}
let hashStrings = [];
- bounds = this.splitBoundingBox(bounds);
+ bounds = bounds.split();
bounds.forEach(function (b) {
+ const c = b.getCoords();
hashStrings = hashStrings.concat(
- nGeohash.bboxes(b.south, b.west, b.north, b.east, precision)
+ nGeohash.bboxes(c.south, c.west, c.north, c.east, precision)
);
});
return hashStrings;
@@ -171,29 +172,10 @@ define([
return this.models.map((geohash) => geohash.get(attr));
},
- /**
- * Splits a given bounding box if it crosses the prime meridian. Returns
- * an array of bounding boxes.
- * @param {Object} bounds - Bounding box object with north, south, east,
- * and west properties.
- * @returns {Object[]} Array of bounding box objects.
- */
- splitBoundingBox: function (bounds) {
- if (!bounds) return [];
- const { north, south, east, west } = bounds;
- if (east < west) {
- return [
- { north, south, east: 180, west },
- { north, south, east, west: -180 },
- ];
- } else {
- return [{ north, south, east, west }];
- }
- },
/**
* Add geohashes to the collection based on a bounding box.
- * @param {Object} bounds - Bounding box with north, south, east, and west
+ * @param {GeoBoundingBox} bounds - Bounding box with north, south, east, and west
* properties.
* @param {boolean} [consolidate=false] - Whether to consolidate the
* geohashes into the smallest set of geohashes that cover the same area.
@@ -231,7 +213,7 @@ define([
maxGeohashes
);
} else {
- const area = this.getBoundingBoxArea(bounds);
+ const area = bounds.getArea();
const precision = this.getMaxPrecision(
area,
maxGeohashes,
@@ -289,53 +271,6 @@ define([
return this.precisionAreas;
},
- /**
- * Get the area of a bounding box in degrees.
- * @param {Object} bounds - Bounding box with north, south, east, and west
- * properties.
- * @returns {Number} The area of the bounding box in degrees.
- */
- getBoundingBoxArea: function (bounds) {
- if (!this.boundsAreValid(bounds)) {
- console.warn(
- `Bounds are invalid: ${JSON.stringify(bounds)}. ` +
- `Returning the globe's area for the given bounding box.`
- );
- return 360 * 180;
- }
- const { north, south, east, west } = bounds;
- // Account for cases where east < west, due to the bounds crossing the
- // prime meridian
- const lonDiff = east < west ? 360 - (west - east) : east - west;
- const latDiff = north - south;
- return Math.abs(latDiff * lonDiff);
- },
-
- /**
- * Check that a bounds object is valid for the purposes of other methods
- * in this Collection.
- * @param {Object} bounds - Bounding box with north, south, east, and west
- * properties.
- * @returns {boolean} Whether the bounds object is valid.
- */
- boundsAreValid: function (bounds) {
- return (
- bounds &&
- typeof bounds.north === "number" &&
- typeof bounds.south === "number" &&
- typeof bounds.east === "number" &&
- typeof bounds.west === "number" &&
- bounds.north <= 90 &&
- bounds.north >= -90 &&
- bounds.south >= -90 &&
- bounds.south <= 90 &&
- bounds.east <= 180 &&
- bounds.east >= -180 &&
- bounds.west >= -180 &&
- bounds.west <= 180
- );
- },
-
/**
* Given a bounding box, estimate the maximum geohash precision that can
* be used to cover the area without exceeding a specified number of
@@ -426,8 +361,8 @@ define([
/**
* Get the optimal range of precision levels to consider using for a given
* bounding box. See {@link getMaxPrecision} and {@link getMinPrecision}.
- * @param {Object} bounds - Bounding box with north, south, east, and west
- * properties.
+ * @param {GeoBoundingBox} bounds - Bounding box with north, south, east,
+ * and west properties.
* @param {Number} maxGeohashes - The maximum number of geohashes that can
* be used to cover the area.
* @param {Number} absMin - The absolute minimum precision level to
@@ -443,7 +378,7 @@ define([
absMin = this.MIN_PRECISION,
absMax = this.MAX_PRECISION
) {
- if (!this.boundsAreValid(bounds)) {
+ if (!bounds.isValid()){
console.warn(
`Bounds are invalid: ${JSON.stringify(bounds)}. ` +
`Returning the min and max allowable precision levels.`
@@ -452,7 +387,7 @@ define([
}
absMin = this.validatePrecision(absMin);
absMax = this.validatePrecision(absMax);
- const area = this.getBoundingBoxArea(bounds);
+ const area = bounds.getArea();
const minP = this.getMinPrecision(area, absMin, absMax);
if (minP === absMax || maxGeohashes === Infinity) return [minP, absMax];
return [minP, this.getMaxPrecision(area, maxGeohashes, minP, absMax)];
@@ -463,7 +398,7 @@ define([
* bounding box. This will return the optimal set of potentially
* mixed-precision geohashes that cover the bounding box at the highest
* precision possible without exceeding the maximum number of geohashes.
- * @param {Object} bounds - Bounding box with north, south, east, and west
+ * @param {GeoBoundingBox} bounds - Bounding box with north, south, east, and west
* properties.
* @param {Number} [minPrecision] - The minimum precision level to
* consider when calculating the optimal set of geohashes. Defaults to the
@@ -485,7 +420,7 @@ define([
maxGeohashes = Infinity
) {
// Check the inputs
- if (!this.boundsAreValid(bounds)) return [];
+ if (!bounds.isValid()) return [];
minPrecision = this.validatePrecision(minPrecision);
maxPrecision = this.validatePrecision(maxPrecision);
if (minPrecision > maxPrecision) minPrecision = maxPrecision;
@@ -502,35 +437,23 @@ define([
// Base32 is the set of characters used to encode geohashes
const base32 = [..."0123456789bcdefghjkmnpqrstuvwxyz"];
- // In case the bounding box crosses the prime meridian, split it in two
- const allBounds = this.splitBoundingBox(bounds);
-
// If the bounds cover the world, return the base set of geohashes
- if (bounds.north >= 90 && bounds.south <= -90 && bounds.east >= 180 && bounds.west <= -180) {
+ if (bounds.coversEarth()) {
return base32;
}
- // Checks if the given bounds are fully within the bounding box
- function fullyContained(n, e, s, w, north, east, south, west) {
- return s >= south && w >= west && n <= north && e <= east;
- }
-
- // Checks if the given bounds are fully outside the bounding box, assuming that
- function fullyOutside(n, e, s, w, north, east, south, west) {
- return n < south || s > north || e < west || w > east;
- }
-
// Checks if a hash is fully contained, fully outside, or overlapping
- // the bounding box
+ // the bounding box. In case the bounding box crosses the prime
+ // meridian, split it in two
+ const allBounds = bounds.split();
function hashPlacement(hash) {
let [s, w, n, e] = nGeohash.decode_bbox(hash);
let outside = [];
for (const b of allBounds) {
- if (fullyContained(n, e, s, w, b.north, b.east, b.south, b.west)) {
+ if (bounds.boundsAreFullyContained(n, e, s, w)) {
return "inside";
} else if (
- fullyOutside(n, e, s, w, b.north, b.east, b.south, b.west)
- ) {
+ bounds.boundsAreFullyOutside(n, e, s, w)) {
outside.push(true);
}
}
@@ -596,12 +519,12 @@ define([
/**
* Get a subset of geohashes from this collection that are within the
* provided bounding box.
- * @param {Object} bounds - Bounding box with north, south, east, and west
+ * @param {GeoBoundingBox} bounds - Bounding box with north, south, east, and west
* properties.
* @returns {Geohashes} Subset of geohashes.
*/
getSubsetByBounds: function (bounds) {
- if (!this.boundsAreValid(bounds)) {
+ if (!bounds || !bounds.isValid()) {
console.warn(
`Bounds are invalid: ${JSON.stringify(bounds)}. ` +
`Returning an empty Geohashes collection.`
diff --git a/src/js/collections/maps/MapAssets.js b/src/js/collections/maps/MapAssets.js
index 262ff3c2a..bfea0be14 100644
--- a/src/js/collections/maps/MapAssets.js
+++ b/src/js/collections/maps/MapAssets.js
@@ -55,7 +55,7 @@ define([
model: Cesium3DTileset,
},
{
- types: ["GeoJsonDataSource", "CzmlDataSource"],
+ types: ["GeoJsonDataSource", "CzmlDataSource", "CustomDataSource"],
model: CesiumVectorData,
},
{
@@ -142,12 +142,8 @@ define([
this.each(function (mapAssetModel) {
mapAssetModel.set("mapModel", mapModel);
});
- } catch (error) {
- console.log(
- "Failed to set the map model on a MapAssets collection" +
- ". Error details: " +
- error
- );
+ } catch (e) {
+ console.log("Failed to set the map model on MapAssets collection", e);
}
},
@@ -236,6 +232,7 @@ define([
* @since 2.25.0
*/
getFeatureAttributes: function (features) {
+ if (!Array.isArray(features)) features = [features];
return features.map((feature) => {
const asset = this.findAssetWithFeature(feature);
return asset?.getFeatureAttributes(feature);
diff --git a/src/js/models/connectors/Filters-Map.js b/src/js/models/connectors/Filters-Map.js
index 505d89a62..266b05073 100644
--- a/src/js/models/connectors/Filters-Map.js
+++ b/src/js/models/connectors/Filters-Map.js
@@ -47,9 +47,9 @@ define(["backbone", "collections/Filters", "models/maps/Map"], function (
*/
defaults: function () {
return {
- filters: new Filters([], { catalogSearch: true }),
+ filters: null,
spatialFilters: [],
- map: new Map(),
+ map: null,
isConnected: false,
onMoveEnd: this.updateSpatialFilters,
};
@@ -68,6 +68,12 @@ define(["backbone", "collections/Filters", "models/maps/Map"], function (
*/
initialize: function (attr, options) {
try {
+ if (!this.get("filters")) {
+ this.set("filters", new Filters([], { catalogSearch: true }));
+ }
+ if (!this.get("map")) {
+ this.set("map", new Map());
+ }
const add = options?.addSpatialFilter ?? true;
this.findAndSetSpatialFilters(add);
} catch (e) {
@@ -177,8 +183,9 @@ define(["backbone", "collections/Filters", "models/maps/Map"], function (
if (resetSpatialFilter) {
this.resetSpatialFilter();
}
+ const interactions = this.get("map")?.get("interactions");
this.stopListening(this.get("filters"), "add remove");
- this.stopListening(this.get("map"), "moveEnd moveStart");
+ this.stopListening(interactions, "moveEnd moveStartAndChanged");
this.set("isConnected", false);
} catch (e) {
console.log("Error stopping Filter-Map listeners: ", e);
@@ -186,23 +193,24 @@ define(["backbone", "collections/Filters", "models/maps/Map"], function (
},
/**
- * Starts listening to the Map model for changes in the
- * 'currentViewExtent' attribute, and calls the updateSpatialFilters
- * function when changes are detected. This method needs to be called for
- * the connector to work.
+ * Starts listening to the Map Interaction model for changes in the
+ * 'viewExtent' attribute, and calls the updateSpatialFilters function
+ * when changes are detected. This method needs to be called for the
+ * connector to work.
*/
connect: function () {
try {
this.disconnect();
const map = this.get("map");
+ const interactions = map.get("interactions");
// Constrain the spatial filter to the current map extent right away
this.updateSpatialFilters();
// Trigger a 'changing' event on the filters collection to
// indicate that the spatial filter is being updated
- this.listenTo(map, "moveStart", function () {
+ this.listenTo(interactions, "moveStartAndChanged", function () {
this.get("filters").trigger("changing");
});
- this.listenTo(map, "moveEnd", function () {
+ this.listenTo(interactions, "moveEnd", function () {
const moveEndFunc = this.get("onMoveEnd");
if (typeof moveEndFunc === "function") {
moveEndFunc.call(this);
@@ -220,7 +228,12 @@ define(["backbone", "collections/Filters", "models/maps/Map"], function (
updateSpatialFilters: function () {
try {
const map = this.get("map");
- const extent = map.get("currentViewExtent");
+ // TODO: If we update the spatialFilter to use the GeoBoundingBox
+ // model (instead of north, south, east, west attributes), then we can
+ // point directly to the MapInteraction model's 'viewExtent' attribute
+ // instead of getting the extent from the map. They will stay in sync
+ // automatically.
+ const extent = map.get("interactions").get("viewExtent").toJSON();
const spatialFilters = this.get("spatialFilters");
if (!spatialFilters?.length) {
diff --git a/src/js/models/connectors/GeoPoints-Cesium.js b/src/js/models/connectors/GeoPoints-Cesium.js
new file mode 100644
index 000000000..f224c6d31
--- /dev/null
+++ b/src/js/models/connectors/GeoPoints-Cesium.js
@@ -0,0 +1,167 @@
+"use strict";
+
+/*global define */
+define([
+ "backbone",
+ "cesium",
+ "collections/maps/GeoPoints",
+ "models/maps/assets/CesiumVectorData",
+], function (Backbone, Cesium, GeoPoints, CesiumVectorData) {
+ /**
+ * @class GeoPointsCesiumConnector
+ * @classdesc This is the base model for other connectors that create geometry
+ * in Cesium based on points in a GeoPoints collection.
+ * @name GeoPointsCesiumConnector
+ * @extends Backbone.Model
+ * @constructor
+ * @classcategory Models/Connectors
+ * @since x.x.x
+ */
+ return Backbone.Model.extend(
+ /** @lends GeoPointsCesiumConnector.prototype */ {
+ /**
+ * The type of Backbone.Model this is.
+ * @type {string}
+ * @default "GeoPointsCesiumConnector"
+ */
+ type: "GeoPointsCesiumConnector",
+
+ /**
+ * Extends the default Backbone.Model.defaults() function to specify
+ * default attributes for the GeoPointsCesiumConnector model.
+ * @returns {Object} The default attributes
+ * @property {GeoPoints} geoPoints - The points collection to visualize
+ * @property {CesiumVectorData} layer - The CesiumVectorData model to use
+ * to visualize the points. This must be a CesiumVectorData model.
+ * @property {Boolean} isConnected - Whether the layer is currently being
+ * updated with changes to the points collection.
+ */
+ defaults: function () {
+ return {
+ geoPoints: null,
+ layer: null,
+ isConnected: false,
+ };
+ },
+
+ /**
+ * Initialize the model.
+ * @param {Object} attrs - The attributes for this model.
+ * @param {GeoPoints | Array} [attributes.geoPoints] - The GeoPoints
+ * collection to use for this connector or an array of JSON attributes to
+ * create a new GeoPoints collection. If not provided, a new empty
+ * GeoPoints collection will be created.
+ * @param {CesiumVectorData | Object} [attributes.layer] - The
+ * CesiumVectorData CesiumVectorData model to use for this connector or a
+ * JSON object with options to create a model. If not provided, a new
+ * layer will be created.
+ */
+ initialize: function (attrs) {
+ try {
+ attrs = attrs || {};
+ this.setGeoPoints(attrs.geoPoints);
+ this.setLayer(attrs.layer);
+ if (attrs.isConnected) {
+ this.connect();
+ }
+ } catch (e) {
+ console.log("Error initializing a GeoPointsCesiumConnector", e);
+ }
+ },
+
+ /**
+ * Set or create and set the GeoPoints collection for this connector.
+ * @param {GeoPoints | Object} [points] - The GeoPoints collection to use
+ * for this connector or an array of JSON attributes to create points.
+ * @returns {GeoPoints} The GeoPoints collection for this connector.
+ */
+ setGeoPoints: function (geoPoints) {
+ if (geoPoints instanceof GeoPoints) {
+ this.set("geoPoints", geoPoints);
+ } else {
+ this.set("geoPoints", new GeoPoints(geoPoints));
+ }
+ return this.get("geoPoints");
+ },
+
+ /**
+ * Set or create and set the CesiumVectorData model for this connector.
+ * @param {CesiumVectorData | Object} [layer] - The CesiumVectorData model
+ * to use for this connector or a JSON object with options to create a new
+ * CesiumVectorData model. If not provided, a new CesiumVectorData model
+ * will be created.
+ * @returns {CesiumVectorData} The CesiumVectorData model for this
+ * connector.
+ */
+ setLayer: function (layer) {
+ if (layer instanceof CesiumVectorData) {
+ this.set("layer", layer);
+ } else {
+ this.set("layer", new CesiumVectorData(layer));
+ }
+ return this.get("layer");
+ },
+
+ /**
+ * Listen for changes to the Points collection and update the
+ * CesiumVectorData model with point entities.
+ */
+ connect: function () {
+ try {
+ this.disconnect();
+
+ // Listen for changes to the points collection and update the layer
+ const geoPoints = this.get("geoPoints");
+ const events = ["update", "reset"];
+
+ events.forEach((eventName) => {
+ this.listenTo(geoPoints, eventName, function (...args) {
+ this.handleCollectionChange(eventName, ...args);
+ });
+ });
+
+ // Restart listeners when points or the layer is replaced
+ this.listenToOnce(this, "change:geoPoints change:layer", () => {
+ if (this.get("isConnected")) {
+ this.connect();
+ }
+ });
+ // Restart listeners when points or the layer is replaced
+ this.listenToOnce(this, "change:geoPoints change:layer", () => {
+ if (this.get("isConnected")) {
+ this.connect();
+ }
+ });
+
+ this.set("isConnected", true);
+ } catch (e) {
+ console.warn("Error connecting Points to Cesium. Disconnecting.", e);
+ this.disconnect();
+ }
+ },
+
+ /**
+ * Stop listening for changes to the Points collection.
+ */
+ disconnect: function () {
+ const geoPoints = this.get("geoPoints");
+ if (geoPoints) this.stopListening(geoPoints);
+ this.set("isConnected", false);
+ },
+
+ /**
+ * Handle add, remove, merge, and reset events from the points collection
+ * @param {"update"|"reset"} eventName - The name of the event
+ * @param {GeoPoints} collection - The points collection
+ * @param {Object} options - Options for the event, as passed by Backbone
+ */
+ handleCollectionChange(eventName, collection, options) {
+ try {
+ // What to do when the collection changes
+ } catch (e) {
+ console.warn('Error handling a "' + eventName + '" event.', e);
+ }
+ },
+ }
+ );
+});
diff --git a/src/js/models/connectors/GeoPoints-CesiumPoints.js b/src/js/models/connectors/GeoPoints-CesiumPoints.js
new file mode 100644
index 000000000..c6354cbc3
--- /dev/null
+++ b/src/js/models/connectors/GeoPoints-CesiumPoints.js
@@ -0,0 +1,169 @@
+"use strict";
+
+/*global define */
+define(["cesium", "models/connectors/GeoPoints-Cesium"], function (
+ Cesium,
+ GeoPointsCesiumConnector
+) {
+ /**
+ * @class GeoPointsCesiumPointsConnector
+ * @classdesc This connector keeps a CesiumVectorData model in sync with the
+ * points in a GeoPoints collection. This connector will listen for changes to
+ * the GeoPoints collection and update the cesiumModel with point entities
+ * created from the points in the collection.
+ * @name GeoPointsCesiumPointsConnector
+ * @extends GeoPointsCesiumConnector
+ * @constructor
+ * @classcategory Models/Connectors
+ * @since x.x.x
+ */
+ return GeoPointsCesiumConnector.extend(
+ /** @lends GeoPointsCesiumPointsConnector.prototype */ {
+ /**
+ * The type of Backbone.Model this is.
+ * @type {string}
+ * @default "GeoPointsCesiumPointsConnector"
+ */
+ type: "GeoPointsCesiumPointsConnector",
+
+ /**
+ * Extends the default Backbone.Model.defaults() function to specify
+ * default attributes for the GeoPointsCesiumPointsConnector model.
+ * @extends GeoPointsCesiumConnector.defaults
+ * @returns {Object} The default attributes
+ * @property {Array} layerPoints - The list of point entities that have
+ * been added to the layer.
+ */
+ defaults: function () {
+ return {
+ // extend the defaults from the parent class
+ ...GeoPointsCesiumConnector.prototype.defaults(),
+ layerPoints: [],
+ };
+ },
+
+ /**
+ * Handle add, remove, merge, and reset events from the points collection
+ * @param {"update"|"reset"} eventName - The name of the event
+ * @param {GeoPoints} collection - The points collection
+ * @param {Object} options - Options for the event, as passed by Backbone
+ */
+ handleCollectionChange(eventName, collection, options) {
+ try {
+ // For merges and resets, just remove all points and re-add them
+ if (!options?.add && !options?.remove) {
+ this.resetLayerPoints();
+ return;
+ }
+ // For adds and removes, just add or remove the points that changed
+ if (eventName === "update") {
+ if (options.add) {
+ const newModels = options.changes.added;
+ newModels.forEach((model) => {
+ this.addLayerPoint(model);
+ });
+ }
+ if (options.remove) {
+ const removedModels = options.changes.removed;
+ removedModels.forEach((model) => {
+ this.removeLayerPoint(model);
+ });
+ }
+ }
+ } catch (e) {
+ console.warn('Error handling a "' + eventName + '" event.', e);
+ }
+ },
+
+ /**
+ * Resync the layer points with the points from the points collection.
+ * This removes all point entities previously added to the layer and adds
+ * new ones for each point in the points collection.
+ */
+ resetLayerPoints: function () {
+ const layer = this.get("layer");
+ layer.suspendEvents();
+ this.removeAllLayerPoints();
+ this.addAllLayerPoints();
+ layer.resumeEvents();
+ },
+
+ /**
+ * Remove all layer points previously added to the layer.
+ * @returns {Boolean} Whether the layer points were removed
+ */
+ removeAllLayerPoints: function () {
+ const layer = this.get("layer");
+ if (!layer) return false;
+ const layerPoints = this.get("layerPoints");
+ layerPoints.forEach((entity) => {
+ layer.removeEntity(entity);
+ });
+ return true;
+ },
+
+ /**
+ * Add all points from the points collection to the layer.
+ * @returns {Boolean} Whether the layer points were added
+ */
+ addAllLayerPoints: function () {
+ const layer = this.get("layer");
+ if (!layer) return false;
+ const geoPoints = this.get("geoPoints");
+ geoPoints.each((model) => {
+ this.addLayerPoint(model);
+ });
+ return true;
+ },
+
+ /**
+ * Add a point from the points collection to the layer. Adds the point
+ * entity to the layerPoints array for tracking.
+ * @param {GeoPoint} model - The point model to add to the layer
+ * @returns {Cesium.Entity} The layer point that was created
+ */
+ addLayerPoint: function (model) {
+ try {
+ const layer = this.get("layer") || this.setLayer();
+ const layerPoint = layer.addEntity({
+ id: model.cid,
+ position: model.get("mapWidgetCoords"),
+ point: {
+ pixelSize: 2,
+ heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
+ },
+ });
+ // Track the layer point so we can remove it later
+ const layerPoints = this.get("layerPoints");
+ layerPoints.push(layerPoint);
+ return layerPoint;
+ } catch (e) {
+ console.log("Failed to add a point to a CesiumVectorData.", e);
+ }
+ },
+
+ /**
+ * Remove a point from the points collection from the layer. Removes the
+ * point entity from the layerPoints array.
+ * @param {GeoPoint} model - The point model to remove from the layer
+ * @returns {Cesium.Entity} The layer point that was removed
+ */
+ removeLayerPoint: function (model) {
+ try {
+ const layer = this.get("layer");
+ if (!layer) return false;
+ const removedPoint = layer.removeEntity(model.cid);
+ // Remove the layer point from the list of layer points
+ const layerPoints = this.get("layerPoints");
+ const index = layerPoints.indexOf(removedPoint);
+ if (index > -1) {
+ layerPoints.splice(index, 1);
+ }
+ return removedPoint;
+ } catch (e) {
+ console.log("Failed to remove a point from a CesiumVectorData.", e);
+ }
+ },
+ }
+ );
+});
diff --git a/src/js/models/connectors/GeoPoints-CesiumPolygon.js b/src/js/models/connectors/GeoPoints-CesiumPolygon.js
new file mode 100644
index 000000000..7a27854bd
--- /dev/null
+++ b/src/js/models/connectors/GeoPoints-CesiumPolygon.js
@@ -0,0 +1,75 @@
+"use strict";
+
+/*global define */
+define(["cesium", "models/connectors/GeoPoints-Cesium"], function (
+ Cesium,
+ GeoPointsCesiumConnector
+) {
+ /**
+ * @class GeoPointsCesiumPolygonConnector
+ * @classdesc This connector keeps a CesiumVectorData model in sync with the
+ * points in a GeoPoints collection. This connector will listen for changes to
+ * the GeoPoints collection and update the cesiumModel a polygon with vertices
+ * created from the points in the collection.
+ * @name GeoPointsCesiumPolygonConnector
+ * @extends GeoPointsCesiumConnector
+ * @constructor
+ * @classcategory Models/Connectors
+ * @since x.x.x
+ */
+ return GeoPointsCesiumConnector.extend(
+ /** @lends GeoPointsCesiumPolygonConnector.prototype */ {
+ /**
+ * The type of Backbone.Model this is.
+ * @type {string}
+ * @default "GeoPointsCesiumPolygonConnector"
+ */
+ type: "GeoPointsCesiumPolygonConnector",
+
+ /**
+ * Extends the default Backbone.Model.defaults() function to specify
+ * default attributes for the GeoPointsCesiumPolygonConnector model.
+ * @extends GeoPointsCesiumConnector.defaults
+ * @returns {Object} The default attributes
+ * @property {Cesium.Entity} polygon - The polygon entity that has
+ * vertices created from the points in the collection.
+ */
+ defaults: function () {
+ return {
+ // extend the defaults from the parent class
+ ...GeoPointsCesiumConnector.prototype.defaults(),
+ polygon: null,
+ };
+ },
+
+ /**
+ * Create a Cesium.Polygon entity and add it to the layer.
+ * @returns {Cesium.Entity} The Cesium.Polygon entity that was added to
+ * the CesiumVectorData model.
+ */
+ addPolygon: function () {
+ const id = this.cid;
+ const layer = this.get("layer") || this.setVectorLayer();
+ const geoPoints = this.get("geoPoints") || this.setPoints();
+ return layer.addEntity({
+ id: id, // If entity with this ID already exists, it will be updated
+ polygon: {
+ height: null, // <- clamp to ground
+ hierarchy: new Cesium.CallbackProperty(() => {
+ return new Cesium.PolygonHierarchy(geoPoints.asMapWidgetCoords());
+ }, false),
+ },
+ });
+ },
+
+ /**
+ * Reset the positions of the polygon vertices to the current points in
+ * the GeoPoints collection.
+ */
+ handleCollectionChange: function () {
+ this.get("polygon") || this.addPolygon();
+ this.get("layer").updateAppearance();
+ },
+ }
+ );
+});
diff --git a/src/js/models/connectors/Map-Search-Filters.js b/src/js/models/connectors/Map-Search-Filters.js
index cc7557aca..7c411f55d 100644
--- a/src/js/models/connectors/Map-Search-Filters.js
+++ b/src/js/models/connectors/Map-Search-Filters.js
@@ -35,14 +35,14 @@ define([
/**
* The default values for this model.
* @type {object}
- * @property {Map} map
- * @property {SolrResults} searchResults
- * @property {Filters} filters
+ * @property {Map} map - The Map model to use for this connector.
+ * @property {SolrResults} searchResults - The SolrResults model to use
+ * @property {Filters} filters - The Filters model to use for this
*/
defaults: function () {
return {
- map: new Map(),
- searchResults: new SearchResults(),
+ map: null,
+ searchResults: null,
filterGroups: [],
filters: null,
};
@@ -83,7 +83,7 @@ define([
initialize: function (attrs, options = {}) {
if (!options) options = {};
const app = MetacatUI.appModel;
- const map = options.map || app.get("catalogSearchMapOptions");
+ const map = options.map || app.get("catalogSearchMapOptions") || {};
const searchResults = options.searchResults || null;
const filterGroups =
options.filterGroups || app.get("defaultFilterGroups");
@@ -213,6 +213,7 @@ define([
* so that they work together.
*/
connect: function () {
+ this.disconnect();
this.coordinateMoveEndSearch();
this.getConnectors().forEach((connector) => connector.connect());
},
@@ -242,6 +243,7 @@ define([
this.resetMoveEndSearch();
const map = this.get("map");
+ const interactions = map.get("interactions");
const mapConnectors = this.getMapConnectors();
// Stop the sub-connectors from doing anything on moveEnd by setting
@@ -253,7 +255,7 @@ define([
// Set the single moveEnd listener here, and run the default moveEnd
// behaviour for each sub-connector. This effectively triggers only one
// search per moveEnd.
- this.listenTo(map, "moveEnd", function () {
+ this.listenTo(interactions, "moveEnd", function () {
mapConnectors.forEach((connector) => {
const moveEndFunc = connector.defaults().onMoveEnd;
if (typeof moveEndFunc === "function") {
@@ -270,7 +272,7 @@ define([
* @see coordinateMoveEndSearch
*/
resetMoveEndSearch: function () {
- this.stopListening(this.get("map"), "moveEnd");
+ this.stopListening(this.get("map").get("interactions"), "moveEnd");
const mapConnectors = this.getMapConnectors();
mapConnectors.forEach((connector) => {
connector.set("onMoveEnd", connector.defaults().onMoveEnd);
diff --git a/src/js/models/connectors/Map-Search.js b/src/js/models/connectors/Map-Search.js
index 827593203..7b9fd4483 100644
--- a/src/js/models/connectors/Map-Search.js
+++ b/src/js/models/connectors/Map-Search.js
@@ -163,6 +163,7 @@ define([
const searchResults = this.get("searchResults");
const map = this.get("map");
+ const interactions = map.get("interactions");
// Pass the facet counts to the GeoHash layer when the search results
// are returned.
@@ -178,12 +179,12 @@ define([
// When the user is panning/zooming in the map, hide the GeoHash layer
// to indicate that the map is not up to date with the search results,
// which are about to be updated.
- this.listenTo(map, "moveStart", this.hideGeoHashLayer);
+ this.listenTo(interactions, "moveStartAndChanged", this.hideGeoHashLayer);
// When the user is done panning/zooming in the map, show the GeoHash
// layer again and update the search results (thereby updating the
// facet counts on the GeoHash layer)
- this.listenTo(map, "moveEnd", function () {
+ this.listenTo(interactions, "moveEnd", function () {
const moveEndFunc = this.get("onMoveEnd");
if (typeof moveEndFunc === "function") {
moveEndFunc.call(this);
@@ -206,8 +207,6 @@ define([
* See {@link MapSearchFiltersConnector#onMoveEnd}
*/
onMoveEnd: function () {
- const searchResults = this.get("searchResults");
- const map = this.get("map");
this.showGeoHashLayer();
this.updateFacet();
},
@@ -237,10 +236,11 @@ define([
*/
disconnect: function () {
const map = this.get("map");
+ const interactions = map?.get("interactions");
const searchResults = this.get("searchResults");
this.stopListening(searchResults, "update reset");
this.stopListening(searchResults, "change:showOnMap");
- this.stopListening(map, "moveStart moveEnd");
+ this.stopListening(interactions, "moveStartAndChanged moveEnd");
this.stopListening(searchResults, "request");
this.set("isConnected", false);
},
diff --git a/src/js/models/filters/SpatialFilter.js b/src/js/models/filters/SpatialFilter.js
index d380387fe..c49573c52 100644
--- a/src/js/models/filters/SpatialFilter.js
+++ b/src/js/models/filters/SpatialFilter.js
@@ -1,11 +1,9 @@
define([
"underscore",
"jquery",
- "backbone",
"models/filters/Filter",
- "collections/maps/Geohashes",
- "collections/Filters",
-], function (_, $, Backbone, Filter, Geohashes, Filters) {
+ "collections/maps/Geohashes"
+], function (_, $, Filter, Geohashes) {
/**
* @classdesc A SpatialFilter represents a spatial constraint on the query to
* be executed.
@@ -103,30 +101,50 @@ define([
}
},
+ /**
+ * Remove the listeners.
+ * @since x.x.x
+ */
+ removeListeners: function () {
+ const extentEvents =
+ "change:height change:north change:south change:east change:west";
+ this.stopListening(this, extentEvents);
+ },
+
/**
* Set a listener that updates the filter when the coordinates & height
* change
* @since 2.25.0
*/
setListeners: function () {
+ this.removeListeners();
const extentEvents =
"change:height change:north change:south change:east change:west";
- this.stopListening(this, extentEvents);
this.listenTo(this, extentEvents, this.updateFilterFromExtent);
},
/**
* Convert the coordinate attributes to a bounds object
- * @returns {object} An object with north, south, east, and west props
+ * @param {string} [as="object"] - The format to return the bounds in.
+ * Defaults to "object". Can set to GeoBoundingBox to return a Backbone
+ * model instead.
+ * @returns {object|GeoBoundingBox} An object with north, south, east, and west props or
+ * a GeoBoundingBox model
* @since 2.25.0
*/
- getBounds: function () {
- return {
+ getBounds: function (as="object") {
+ const coords = {
north: this.get("north"),
south: this.get("south"),
east: this.get("east"),
west: this.get("west"),
};
+ if (as === "GeoBoundingBox") {
+ const GeoBoundingBox = require("models/maps/GeoBoundingBox");
+ return new GeoBoundingBox(coords);
+ } else {
+ return coords;
+ }
},
/**
@@ -135,13 +153,7 @@ define([
* @since 2.25.0
*/
coversEarth: function () {
- const bounds = this.getBounds();
- return (
- bounds.north === 90 &&
- bounds.south === -90 &&
- bounds.east === 180 &&
- bounds.west === -180
- );
+ const bounds = this.getBounds("GeoBoundingBox").coversEarth();
},
/**
@@ -163,7 +175,7 @@ define([
}
const geohashes = new Geohashes();
- const bounds = this.getBounds();
+ const bounds = this.getBounds("GeoBoundingBox");
const limit = this.get("maxGeohashValues");
geohashes.addGeohashesByBounds(bounds, true, limit, true);
this.set({
@@ -316,7 +328,13 @@ define([
* @inheritdoc
*/
resetValue: function () {
- const df = this.defaults();
+ // Need to stop listeners because otherwise changing the coords will
+ // update the filter values. This sometimes updates the values *after*
+ // the values are reset, preventing the reset from working.
+ this.removeListeners();
+
+ let df = this.defaults();
+
this.set({
values: df.values,
east: df.east,
@@ -325,6 +343,9 @@ define([
south: df.south,
height: df.height,
});
+
+ // Reset the listeners
+ this.setListeners();
},
}
);
diff --git a/src/js/models/maps/Feature.js b/src/js/models/maps/Feature.js
index f68c61e4a..66b2b0fc6 100644
--- a/src/js/models/maps/Feature.js
+++ b/src/js/models/maps/Feature.js
@@ -1,190 +1,156 @@
-'use strict';
-
-define(
- [
- 'jquery',
- 'underscore',
- 'backbone'
- ],
- function (
- $,
- _,
- Backbone
- ) {
- /**
- * @classdesc A Feature Model organizes information about a single feature of a vector
- * layer in a map.
- * @classcategory Models/Maps
- * @class Feature
- * @name Feature
- * @extends Backbone.Model
- * @since 2.18.0
- * @constructor
- */
- var Feature = Backbone.Model.extend(
- /** @lends Feature.prototype */ {
-
- /**
- * The name of this type of model
- * @type {string}
- */
- type: 'Feature',
-
- /**
- * Default attributes for Feature models
- * @name Feature#defaults
- * @type {Object}
- * @property {Object} properties Property names (keys) and property values
- * (values) for properties set on this feature. For example, the properties that
- * would be in an attributes table for a shapefile.
- * @property {MapAsset} mapAsset If the feature is part of a Map Asset, then the
- * model for that asset. For example, if this is a feature if a 3D tileset, then
- * the Cesium3DTileset map asset model.
- * @property {string|number} featureID An ID that's used to identify this feature
- * in the map. It should be unique among all map features. (In Cesium, this is the
- * Pick ID key.)
- * @property {*} featureObject The object that a Map widget uses to represent this
- * feature in the map. For example, in Cesium this could be a
- * Cesium.Cesium3DTileFeature or a Cesium.Entity.
- * @property {string} label An optional friendly label or name for this feature.
- */
- defaults: function () {
- return {
- properties: {},
- mapAsset: null,
- featureID: null,
- featureObject: null,
- label: null
+"use strict";
+
+define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
+ /**
+ * @classdesc A Feature Model organizes information about a single feature of
+ * a vector layer in a map.
+ * @classcategory Models/Maps
+ * @class Feature
+ * @name Feature
+ * @extends Backbone.Model
+ * @since 2.18.0
+ * @constructor
+ */
+ var Feature = Backbone.Model.extend(
+ /** @lends Feature.prototype */ {
+ /**
+ * The name of this type of model
+ * @type {string}
+ */
+ type: "Feature",
+
+ /**
+ * Default attributes for Feature models
+ * @name Feature#defaults
+ * @type {Object}
+ * @property {Object} properties Property names (keys) and property values
+ * (values) for properties set on this feature. For example, the
+ * properties that would be in an attributes table for a shapefile.
+ * @property {MapAsset} mapAsset If the feature is part of a Map Asset,
+ * then the model for that asset. For example, if this is a feature if a
+ * 3D tileset, then the Cesium3DTileset map asset model.
+ * @property {string|number} featureID An ID that's used to identify this
+ * feature in the map. It should be unique among all map features. (In
+ * Cesium, this is the Pick ID key.)
+ * @property {*} featureObject The object that a Map widget uses to
+ * represent this feature in the map. For example, in Cesium this could be
+ * a Cesium.Cesium3DTileFeature or a Cesium.Entity.
+ * @property {string} label An optional friendly label or name for this
+ * feature.
+ */
+ defaults: function () {
+ return {
+ properties: {},
+ mapAsset: null,
+ featureID: null,
+ featureObject: null,
+ label: null,
+ };
+ },
+
+ /**
+ * Checks if the attributes for this model are only the default
+ * attributes.
+ * @returns {boolean} Returns true if all of the attributes are equal to
+ * the default attributes, false otherwise.
+ */
+ isDefault: function () {
+ try {
+ var defaults = this.defaults();
+ var current = this.attributes;
+ return _.isEqual(defaults, current);
+ } catch (error) {
+ console.log(
+ "Failed to check if a Feature model is the default.",
+ error
+ );
+ }
+ },
+
+ /**
+ * Clears all the model attributes and sets them to the default values.
+ * This will trigger a change event. No event is triggered if all of the
+ * value are already set to default.
+ */
+ setToDefault: function () {
+ try {
+ // Don't make changes if model is already the default
+ if (!this.isDefault()) {
+ this.clear({ silent: true });
+ this.set(this.defaults());
}
- },
-
- // /**
- // * Executed when a new Feature model is created.
- // * @param {Object} [attributes] The initial values of the attributes, which will
- // * be set on the model.
- // * @param {Object} [options] Options for the initialize function.
- // */
- // initialize: function (attributes, options) {
- // try {
-
- // }
- // catch (error) {
- // console.log(
- // 'There was an error initializing a Feature model' +
- // '. Error details: ' + error
- // );
- // }
- // },
-
- /**
- * Checks if the attributes for this model are only the default attributes.
- * @returns {boolean} Returns true if all of the attributes are equal to the
- * default attributes, false otherwise.
- */
- isDefault: function () {
- try {
- var defaults = this.defaults()
- var current = this.attributes
- return _.isEqual(defaults, current)
+ } catch (error) {
+ console.log(
+ "There was an error reset a Feature model to default" +
+ ". Error details: " +
+ error
+ );
+ }
+ },
+
+ /**
+ * Given an map-widget-specific-object representing a feature, and a
+ * MapAssets collection, this function will attempt to find the
+ * corresponding MapAsset model in the collection, and extract the
+ * feature attributes from that model.
+ * @param {*} feature - An object representing a feature in the map. For
+ * example, in Cesium this could be a Cesium.Cesium3DTileFeature or a
+ * Cesium.Entity.
+ * @param {MapAssets} assets - A MapAssets collection to use to extract
+ * feature attributes from a feature object.
+ * @returns {object} - The JSON object of all the Feature attributes
+ * @since x.x.x
+ */
+ attrsFromFeatureObject: function (feature, assets) {
+ if (feature instanceof Feature) {
+ return feature.clone().attributes;
+ }
+ let attrs = null;
+ // if this is already an object with feature attributes, return it
+ if (typeof feature == "object") {
+ if (
+ feature.hasOwnProperty("mapAsset") &&
+ feature.hasOwnProperty("properties")
+ ) {
+ attrs = feature;
+ } else if (assets) {
+ attrs = assets.getFeatureAttributes([feature])[0];
}
- catch (error) {
- console.log(
- 'There was an error checking if a Feature model has only default attributes in a Feature' +
- '. Error details: ' + error
+ }
+ return attrs;
+ },
+
+ /**
+ * Parses the given input into a JSON object to be set on the model. If
+ * passed a MapAssets collection as an option, and the input includes an
+ * assets property, then the parse function will attempt to find the
+ * feature object's corresponding MapAsset model in the collection, and
+ * extract the feature attributes from that model.
+ *
+ * @param {object} input - The raw response object
+ * @param {object} [options] - Options for the parse function
+ * @property {MapAssets} [options.assets] - A MapAssets collection to use
+ * to extract feature attributes from a feature object.
+ * @return {object} - The JSON object of all the Feature attributes
+ */
+ parse: function (input, options) {
+ try {
+ if (!input) return null;
+ if (input.featureObject && options.assets) {
+ const attrs = this.attrsFromFeatureObject(
+ input.featureObject,
+ options.assets
);
+ input = Object.assign({}, input, attrs);
}
- },
-
- /**
- * Clears all the model attributes and sets them to the default values. This will
- * trigger a change event. No event is triggered if all of the value are already
- * set to default.
- */
- setToDefault: function () {
- try {
- // Don't make changes if model is already the default
- if (!this.isDefault()) {
- this.clear({ silent: true })
- this.set(this.defaults())
- }
- }
- catch (error) {
- console.log(
- 'There was an error reset a Feature model to default' +
- '. Error details: ' + error
- );
- }
- },
-
- // /**
- // * Parses the given input into a JSON object to be set on the model.
- // *
- // * @param {TODO} input - The raw response object
- // * @return {TODO} - The JSON object of all the Feature attributes
- // */
- // parse: function (input) {
-
- // try {
-
- // var modelJSON = {};
-
- // return modelJSON
-
- // }
- // catch (error) {
- // console.log(
- // 'There was an error parsing a Feature model' +
- // '. Error details: ' + error
- // );
- // }
-
- // },
-
- // /**
- // * Overrides the default Backbone.Model.validate.function() to check if this if
- // * the values set on this model are valid.
- // *
- // * @param {Object} [attrs] - A literal object of model attributes to validate.
- // * @param {Object} [options] - A literal object of options for this validation
- // * process
- // *
- // * @return {Object} - Returns a literal object with the invalid attributes and
- // * their corresponding error message, if there are any. If there are no errors,
- // * returns nothing.
- // */
- // validate: function (attrs, options) {
- // try {
-
- // }
- // catch (error) {
- // console.log(
- // 'There was an error validating a Feature model' +
- // '. Error details: ' + error
- // );
- // }
- // },
-
- // /**
- // * Creates a string using the values set on this model's attributes.
- // * @return {string} The Feature string
- // */
- // serialize: function () {
- // try {
- // var serializedFeature = '';
-
- // return serializedFeature;
- // }
- // catch (error) {
- // console.log(
- // 'There was an error serializing a Feature model' +
- // '. Error details: ' + error
- // );
- // }
- // },
-
- });
- return Feature;
+ return input;
+ } catch (error) {
+ console.log("Failed to parse a Feature model", error);
+ }
+ },
+ }
+ );
- }
-);
+ return Feature;
+});
diff --git a/src/js/models/maps/GeoBoundingBox.js b/src/js/models/maps/GeoBoundingBox.js
new file mode 100644
index 000000000..87e4b7cb2
--- /dev/null
+++ b/src/js/models/maps/GeoBoundingBox.js
@@ -0,0 +1,182 @@
+"use strict";
+
+define(["backbone"], function (Backbone) {
+ /**
+ * @class GeoBoundingBox
+ * @classdesc The GeoBoundingBox model stores the geographical boundaries for
+ * an area on the Earth's surface. It includes the northernmost, southernmost,
+ * easternmost, and westernmost latitudes and longitudes, as well as an
+ * optional height parameter.
+ * @classcategory Models/Maps
+ * @name GeoBoundingBox
+ * @since x.x.x
+ * @extends Backbone.Model
+ */
+ var GeoBoundingBox = Backbone.Model.extend(
+ /** @lends GeoBoundingBox.prototype */ {
+ /**
+ * The type of model this is.
+ * @type {String}
+ */
+ type: "GeoBoundingBox",
+
+ /**
+ * Overrides the default Backbone.Model.defaults() function to specify
+ * default attributes for the GeoBoundingBox.
+ * @property {number|null} north The northernmost latitude of the bounding
+ * box. Should be a number between -90 and 90.
+ * @property {number|null} south The southernmost latitude of the bounding
+ * box. Should be a number between -90 and 90.
+ * @property {number|null} east The easternmost longitude of the bounding
+ * box. Should be a number between -180 and 180.
+ * @property {number|null} west The westernmost longitude of the bounding
+ * box. Should be a number between -180 and 180.
+ * @property {number|null} [height] The height of the camera above the
+ * bounding box. Represented in meters above sea level. This attribute is
+ * optional and can be null.
+ */
+ defaults: function () {
+ return {
+ north: null,
+ south: null,
+ east: null,
+ west: null,
+ height: null,
+ };
+ },
+
+ // /**
+ // * Run when a new GeoBoundingBox is created.
+ // * @param {Object} attrs - An object specifying configuration options for
+ // * the GeoBoundingBox. If any config option is not specified, the default
+ // * will be used instead (see {@link GeoBoundingBox#defaults}).
+ // */
+ // initialize: function (attrs, options) {
+ // try {
+ // // ...
+ // } catch (e) {
+ // console.log("Error initializing a GeoBoundingBox model", e);
+ // }
+ // },
+
+ /**
+ * Splits the given bounding box if it crosses the prime meridian.
+ * Returns one or two new GeoBoundingBox models.
+ * @returns {GeoBoundingBox[]} An array of GeoBoundingBox models. One if
+ * the bounding box does not cross the prime meridian, two if it does.
+ */
+ split: function () {
+ const { north, south, east, west } = this.getCoords();
+ if (east < west) {
+ return [
+ new GeoBoundingBox({ north, south, east: 180, west }),
+ new GeoBoundingBox({ north, south, east, west: -180 }),
+ ];
+ } else {
+ return [this.clone()];
+ }
+ },
+
+ /**
+ * Get the area of this bounding box in degrees.
+ * @returns {Number} The area of the bounding box in degrees. Will return
+ * the globe's area if the bounding box is invalid.
+ */
+ getArea: function () {
+ if (!this.isValid()) {
+ console.warn("Invalid bounding box, returning globe area");
+ return 360 * 180;
+ }
+ const { north, south, east, west } = this.attributes;
+ // Account for cases where east < west, due to the bounds crossing the
+ // prime meridian
+ const lonDiff = east < west ? 360 - (west - east) : east - west;
+ const latDiff = north - south;
+ return Math.abs(latDiff * lonDiff);
+ },
+
+ /**
+ * Return the four sides of the bounding box as an array.
+ * @returns {Object} An object with the northernmost, southernmost,
+ * easternmost, and westernmost coordinates of the bounding box.
+ */
+ getCoords: function () {
+ return {
+ north: this.get("north"),
+ south: this.get("south"),
+ east: this.get("east"),
+ west: this.get("west"),
+ };
+ },
+
+ /**
+ * Check if the bounding box covers the entire Earth.
+ * @returns {Boolean} True if the bounding box covers the entire Earth,
+ * false otherwise.
+ */
+ coversEarth: function () {
+ const { north, south, east, west } = this.getCoords();
+ return north >= 90 && south <= -90 && east >= 180 && west <= -180;
+ },
+
+ /**
+ * Check if another bounding box is fully contained within this bounding
+ * box.
+ * @param {Number} n - The northernmost latitude of the bounding box.
+ * @param {Number} e - The easternmost longitude of the bounding box.
+ * @param {Number} s - The southernmost latitude of the bounding box.
+ * @param {Number} w - The westernmost longitude of the bounding box.
+ * @returns {Boolean} True if the other bounding box is fully contained
+ * within this bounding box, false otherwise.
+ */
+ boundsAreFullyContained: function (n, e, s, w) {
+ const { north, south, east, west } = this.getCoords();
+ return s >= south && w >= west && n <= north && e <= east;
+ },
+
+ /**
+ * Check if another bounding box is fully outside of this bounding box.
+ * @param {Number} n - The northernmost latitude of the bounding box.
+ * @param {Number} e - The easternmost longitude of the bounding box.
+ * @param {Number} s - The southernmost latitude of the bounding box.
+ * @param {Number} w - The westernmost longitude of the bounding box.
+ * @returns {Boolean} True if the other bounding box is fully outside this
+ * bounding box, false otherwise.
+ */
+ boundsAreFullyOutside: function (n, e, s, w) {
+ const { north, south, east, west } = this.getCoords();
+ return n < south || s > north || e < west || w > east;
+ },
+
+ /**
+ * Validate the model attributes
+ * @param {Object} attrs - The model's attributes
+ */
+ validate: function (attrs, options) {
+ const bounds = attrs;
+ const isValid =
+ bounds &&
+ typeof bounds.north === "number" &&
+ typeof bounds.south === "number" &&
+ typeof bounds.east === "number" &&
+ typeof bounds.west === "number" &&
+ bounds.north <= 90 &&
+ bounds.north >= -90 &&
+ bounds.south >= -90 &&
+ bounds.south <= 90 &&
+ bounds.east <= 180 &&
+ bounds.east >= -180 &&
+ bounds.west >= -180 &&
+ bounds.west <= 180;
+ if (!isValid) {
+ return (
+ "Bounds must include a number between -90 and 90 for north " +
+ "and south, and between -180 and 180 for east and west."
+ );
+ }
+ },
+ }
+ );
+
+ return GeoBoundingBox;
+});
diff --git a/src/js/models/maps/GeoPoint.js b/src/js/models/maps/GeoPoint.js
new file mode 100644
index 000000000..ea73452a1
--- /dev/null
+++ b/src/js/models/maps/GeoPoint.js
@@ -0,0 +1,139 @@
+"use strict";
+
+define(["backbone", "models/maps/GeoUtilities"], function (
+ Backbone,
+ GeoUtilities
+) {
+ /**
+ * @class GeoPoint
+ * @classdesc The GeoPoint model stores geographical coordinates including
+ * latitude, longitude, and height in meters above sea level.
+ * @classcategory Models/Maps
+ * @name GeoPoint
+ * @since x.x.x
+ * @extends Backbone.Model
+ */
+ var GeoPoint = Backbone.Model.extend(
+ /** @lends GeoPoint.prototype */ {
+ /**
+ * The type of model this is.
+ * @type {String}
+ */
+ type: "GeoPoint",
+
+ /**
+ * Overrides the default Backbone.Model.defaults() function to specify
+ * default attributes for the GeoPoint.
+ * @returns {Object} The default attributes
+ * @property {number} latitude - The latitude of the point in degrees
+ * @property {number} longitude - The longitude of the point in degrees
+ * @property {number} height - The height of the point in meters above sea
+ * level
+ * @property {*} mapWidgetCoords - Optionally, Coordinates in the format
+ * provided by the map widget. For example, for Cesium, this is the Cesium
+ * Cartesian3 ECEF coordinates.
+ */
+ defaults: function () {
+ return {
+ latitude: null,
+ longitude: null,
+ height: null,
+ mapWidgetCoords: null,
+ };
+ },
+
+ /**
+ * Get the long and lat of the point as an array
+ * @returns {Array} An array in the form [longitude, latitude]
+ */
+ to2DArray: function () {
+ return [this.get("longitude"), this.get("latitude")];
+ },
+
+ /**
+ * Convert the point to a GeoJSON geometry object
+ * @returns {Object} A GeoJSON geometry object with the type (Point) and
+ * coordinates of the point
+ */
+ toGeoJsonGeometry: function () {
+ return {
+ type: "Point",
+ coordinates: this.to2DArray(),
+ };
+ },
+
+ /**
+ * Convert the point to a GeoJSON feature object
+ * @returns {Object} A GeoJSON feature object with the type (Feature) and
+ * geometry of the point
+ */
+ toGeoJsonFeature: function () {
+ return {
+ type: "Feature",
+ geometry: this.toGeoJsonGeometry(),
+ properties: {},
+ };
+ },
+
+ /**
+ * Convert the point to a feature in a CZML document
+ * @returns {Object} A CZML feature object with the type (Feature) and
+ * geometry of the point.
+ */
+ toCZML: function () {
+ const ecefCoord = this.toECEFArray();
+ return {
+ id: this.cid,
+ point: {
+ pixelSize: 10,
+ show: true,
+ heightReference: "CLAMP_TO_GROUND",
+ },
+ position: {
+ cartesian: ecefCoord,
+ },
+ };
+ },
+
+ /**
+ * Convert the point to an array of ECEF coordinates
+ * @returns {Array} An array in the form [x, y, z]
+ */
+ toECEFArray: function () {
+ return this.geodeticToECEF(this.to2DArray());
+ },
+
+ /**
+ * Convert a given point to an array of ECEF coordinates
+ * @param {Array} coord - An array in the form [longitude, latitude]
+ * @returns {Array} An array in the form [x, y, z]
+ */
+ geodeticToECEF: function (coord) {
+ return GeoUtilities.prototype.geodeticToECEF(coord);
+ },
+
+ /**
+ * Validate the model attributes
+ * @param {Object} attrs - The model's attributes
+ */
+ validate: function (attrs) {
+ if (attrs.latitude < -90 || attrs.latitude > 90) {
+ return "Invalid latitude. Must be between -90 and 90.";
+ }
+
+ if (attrs.longitude < -180 || attrs.longitude > 180) {
+ return "Invalid longitude. Must be between -180 and 180.";
+ }
+
+ // Assuming height is in meters and can theoretically be below sea
+ // level. Adjust the height constraints as needed for your specific
+ // application.
+ if (typeof attrs.height !== "number") {
+ return "Invalid height. Must be a number.";
+ }
+ },
+ }
+ );
+
+ return GeoPoint;
+});
diff --git a/src/js/models/maps/GeoScale.js b/src/js/models/maps/GeoScale.js
new file mode 100644
index 000000000..ecfd43623
--- /dev/null
+++ b/src/js/models/maps/GeoScale.js
@@ -0,0 +1,62 @@
+"use strict";
+
+define(["backbone"], function (Backbone) {
+ /**
+ * @class GeoScale
+ * @classdesc The GeoScale model stores the relative scale of the map in
+ * meters vs. pixels. This can be used to convert between the two.
+ * @classcategory Models/Maps
+ * @name GeoScale
+ * @since x.x.x
+ * @extends Backbone.Model
+ */
+ var GeoScale = Backbone.Model.extend(
+ /** @lends GeoScale.prototype */ {
+ /**
+ * The type of model this is.
+ * @type {String}
+ */
+ type: "GeoScale",
+
+ /**
+ * Overrides the default Backbone.Model.defaults() function to specify
+ * default attributes for the GeoScale.
+ */
+ defaults: function () {
+ return {
+ pixel: 1,
+ meters: null,
+ };
+ },
+
+ // /**
+ // * Run when a new GeoScale is created.
+ // * @param {Object} attrs - An object specifying configuration options
+ // * for the GeoScale. If any config option is not specified, the default will be
+ // * used instead (see {@link GeoScale#defaults}).
+ // */
+ // initialize: function (attrs, options) {
+ // try {
+ // // ...
+ // } catch (e) {
+ // console.log("Error initializing a GeoScale model", e);
+ // }
+ // },
+
+ /**
+ * Validate the model attributes
+ * @param {Object} attrs - The model's attributes
+ */
+ validate: function (attrs, options) {
+ if (attrs.pixel < 0) {
+ return "Invalid pixel scale. Must be greater than 0.";
+ }
+ if (attrs.meters < 0) {
+ return "Invalid meters scale. Must be greater than 0.";
+ }
+ },
+ }
+ );
+
+ return GeoScale;
+});
diff --git a/src/js/models/maps/GeoUtilities.js b/src/js/models/maps/GeoUtilities.js
new file mode 100644
index 000000000..62e51cddc
--- /dev/null
+++ b/src/js/models/maps/GeoUtilities.js
@@ -0,0 +1,55 @@
+"use strict";
+
+define(["backbone"], function (Backbone) {
+ /**
+ * @class GeoUtilities
+ * @classdesc The GeoUtilities model has methods foe handling spatial data
+ * that are used across multiple models/collections/views, and that don't
+ * belong in any one of them.
+ * @classcategory Models/Maps
+ * @name GeoUtilities
+ * @since x.x.x
+ * @extends Backbone.Model
+ */
+ var GeoUtilities = Backbone.Model.extend(
+ /** @lends GeoUtilities.prototype */ {
+ /**
+ * The type of model this is.
+ * @type {String}
+ */
+ type: "GeoUtilities",
+
+ /**
+ * Convert geodetic coordinates to Earth-Centered, Earth-Fixed (ECEF)
+ * coordinates. Currently this function assumes the WGS-84 ellipsoid,
+ * and does not account for altitude/height (it's assumed the coordinate
+ * is at sea level)
+ * @param {Array} coord The geodetic coordinates in the form [longitude,
+ * latitude].
+ * @returns {Array} The ECEF coordinates.
+ */
+ geodeticToECEF: function (coord) {
+ const a = 6378137; // WGS-84 semi-major axis (meters)
+ const f = 1 / 298.257223563; // WGS-84 flattening
+ const e2 = 2 * f - f * f; // Square of eccentricity
+
+ const lon = coord[0] * (Math.PI / 180); // Convert longitude to radians
+ const lat = coord[1] * (Math.PI / 180); // Convert latitude to radians
+ const alt = 10000;
+ const sinLon = Math.sin(lon);
+ const cosLon = Math.cos(lon);
+ const sinLat = Math.sin(lat);
+ const cosLat = Math.cos(lat);
+
+ const N = a / Math.sqrt(1 - e2 * sinLat * sinLat); // Prime vertical radius of curvature
+ const x = (N + alt) * cosLat * cosLon;
+ const y = (N + alt) * cosLat * sinLon;
+ const z = (N * (1 - e2) + alt) * sinLat;
+
+ return [x, y, z];
+ },
+ }
+ );
+
+ return GeoUtilities;
+});
diff --git a/src/js/models/maps/Geohash.js b/src/js/models/maps/Geohash.js
index 429e3c436..cb213dc65 100644
--- a/src/js/models/maps/Geohash.js
+++ b/src/js/models/maps/Geohash.js
@@ -1,11 +1,12 @@
"use strict";
-define(["jquery", "underscore", "backbone", "nGeohash"], function (
- $,
- _,
- Backbone,
- nGeohash
-) {
+define([
+ "jquery",
+ "underscore",
+ "backbone",
+ "nGeohash",
+ "models/maps/GeoUtilities",
+], function ($, _, Backbone, nGeohash, GeoUtilities) {
/**
* @classdesc A Geohash Model represents a single geohash.
* @classcategory Models/Geohashes
@@ -287,7 +288,7 @@ define(["jquery", "underscore", "backbone", "nGeohash"], function (
/**
* Get the geohash as a CZML Feature.
- * @param {*} label The key for the property to display as a label.
+ * @param {string} label The key for the property to display as a label.
* @returns {Object} A CZML Feature representing the geohash, including
* a polygon of the geohash area and a label with the value of the
* property specified by the label parameter.
@@ -315,9 +316,9 @@ define(["jquery", "underscore", "backbone", "nGeohash"], function (
},
properties: properties,
};
- if (label) {
+ if (label && (properties[label] || properties[label] === 0)) {
(feature["label"] = {
- text: properties[label].toString(),
+ text: properties[label]?.toString(),
show: true,
fillColor: {
rgba: [255, 255, 255, 255],
@@ -346,24 +347,7 @@ define(["jquery", "underscore", "backbone", "nGeohash"], function (
* @returns {Array} The ECEF coordinates.
*/
geodeticToECEF: function (coord) {
- const a = 6378137; // WGS-84 semi-major axis (meters)
- const f = 1 / 298.257223563; // WGS-84 flattening
- const e2 = 2 * f - f * f; // Square of eccentricity
-
- const lon = coord[0] * (Math.PI / 180); // Convert longitude to radians
- const lat = coord[1] * (Math.PI / 180); // Convert latitude to radians
- const alt = 10000;
- const sinLon = Math.sin(lon);
- const cosLon = Math.cos(lon);
- const sinLat = Math.sin(lat);
- const cosLat = Math.cos(lat);
-
- const N = a / Math.sqrt(1 - e2 * sinLat * sinLat); // Prime vertical radius of curvature
- const x = (N + alt) * cosLat * cosLon;
- const y = (N + alt) * cosLat * sinLon;
- const z = (N * (1 - e2) + alt) * sinLat;
-
- return [x, y, z];
+ return GeoUtilities.prototype.geodeticToECEF(coord);
},
}
);
diff --git a/src/js/models/maps/Map.js b/src/js/models/maps/Map.js
index 6b7390d08..f15cfe4a5 100644
--- a/src/js/models/maps/Map.js
+++ b/src/js/models/maps/Map.js
@@ -4,10 +4,9 @@ define([
"jquery",
"underscore",
"backbone",
- "collections/maps/Features",
- "models/maps/Feature",
"collections/maps/MapAssets",
-], function ($, _, Backbone, Features, Feature, MapAssets) {
+ "models/maps/MapInteraction",
+], function ($, _, Backbone, MapAssets, Interactions) {
/**
* @class MapModel
* @classdesc The Map Model contains all of the settings and options for a
@@ -119,8 +118,8 @@ define([
* pitch: -90,
* roll: 0
* }
- */
-
+ */
+
/**
* The type of model this is.
* @type {String}
@@ -143,11 +142,6 @@ define([
* options to show in the map.
* @property {MapAssets} [layers = new MapAssets()] - The imagery and
* vector data to render in the map.
- * @property {Features} [selectedFeatures = new Features()] - Particular
- * features from one or more layers that are highlighted/selected on the
- * map. The 'selectedFeatures' attribute is updated by the map widget
- * (cesium) with a Feature model when a user selects a geographical
- * feature on the map (e.g. by clicking)
* @property {Boolean} [showToolbar=true] - Whether or not to show the
* side bar with layer list and other tools. True by default.
* @property {Boolean} [showLayerList=true] - Whether or not to include
@@ -161,20 +155,6 @@ define([
* scale bar.
* @property {Boolean} [showFeatureInfo=true] - Whether or not to allow
* users to click on map features to show more information about them.
- * @property {Object} [currentPosition={ longitude: null, latitude: null,
- * height: null}] An object updated by the map widget to show the
- * longitude, latitude, and height (elevation) at the position of the
- * mouse on the map. Note: The CesiumWidgetView does not yet update the
- * height property.
- * @property {Object} [currentScale={ meters: null, pixels: null }] An
- * object updated by the map widget that gives two equivalent measurements
- * based on the map's current position and zoom level: The number of
- * pixels on the screen that equal the number of meters on the map/globe.
- * @property {Object} [currentViewExtent={ north: null, east: null, south:
- * null, west: null }] An object updated by the map widget that gives the
- * extent of the current visible area as a bounding box in
- * longitude/latitude coordinates, as well as the height/altitude in
- * meters.
* @property {String} [clickFeatureAction="showDetails"] - The default
* action to take when a user clicks on a feature on the map. The
* available options are "showDetails" (show the feature details in the
@@ -197,29 +177,12 @@ define([
},
]),
terrains: new MapAssets(),
- selectedFeatures: new Features(),
showToolbar: true,
showLayerList: true,
showHomeButton: true,
toolbarOpen: false,
showScaleBar: true,
showFeatureInfo: true,
- currentPosition: {
- longitude: null,
- latitude: null,
- height: null,
- },
- currentScale: {
- meters: null,
- pixels: null,
- },
- currentViewExtent: {
- north: null,
- east: null,
- south: null,
- west: null,
- height: null,
- },
clickFeatureAction: "showDetails",
};
},
@@ -245,94 +208,62 @@ define([
this.set("terrains", new MapAssets(config.terrains));
}
}
+ this.setUpInteractions();
} catch (error) {
- console.log(
- "There was an error initializing a Map model" +
- ". Error details: " +
- error
- );
+ console.log('Failed to initialize a Map model.', error);
}
},
/**
- * Set or unset the selected Features. A selected feature is a polygon,
- * line, point, or other element of vector data that is in focus on the
- * map (e.g. because a user clicked it to show more details.)
- * @param {(Feature|Object[])} features - An array of Feature models or
- * objects with attributes to set on new Feature models. If no features
- * argument is passed to this function, then any currently selected
- * feature will be removed (as long as replace is set to true)
- * @param {Boolean} [replace=true] - If true, any currently selected
- * features will be replaced with the newly selected features. If false,
- * then the newly selected features will be added to any that are
- * currently selected.
+ * Set or replace the MapInteraction model on the map.
+ * @returns {MapInteraction} The new interactions model.
+ * @since x.x.x
*/
- selectFeatures: function (features, replace = true) {
- try {
- const model = this;
-
- // Create a features collection if one doesn't already exist
- if (!model.get("selectedFeatures")) {
- model.set("selectedFeatures", new Features());
- }
-
- // Don't update the selected features collection if the newly selected
- // features are identical.
- const currentFeatures = model.get("selectedFeatures");
- if (
- features &&
- currentFeatures &&
- currentFeatures.length === features.length &&
- currentFeatures.containsFeatures(features)
- ) {
- return;
- }
+ setUpInteractions: function () {
+ const interactions = new Interactions({
+ mapModel: this,
+ });
+ this.set("interactions", interactions);
+ return interactions;
+ },
- // If no feature is passed to this function (and replace is true),
- // then empty the Features collection
- features = !features || !Array.isArray(features) ? [] : features;
+ /**
+ * Select features on the map. Updates the selectedFeatures attribute on
+ * the MapInteraction model.
+ * @param {Feature[]} features - An array of Feature models to select.
+ * since x.x.x
+ */
+ selectFeatures: function (features) {
+ this.get("interactions")?.selectFeatures(features);
+ },
- // Convert the feature objects that are types specific to the map view
- // (Cesium) to a generic Feature model
- features = model.convertFeatures(features);
+ /**
+ * Get the currently selected features on the map.
+ * @returns {Features} The selected Feature collection.
+ * @since x.x.x
+ */
+ getSelectedFeatures: function () {
+ return this.get("interactions")?.get("selectedFeatures");
+ },
- // Update the Feature model with the new selected feature information.
- const newAttrs = features.map(function (feature) {
- return Object.assign(
- {},
- new Feature().defaults(),
- feature.attributes
- );
- });
- model.get("selectedFeatures").set(newAttrs, { remove: replace });
- } catch (e) {
- console.log("Failed to select a Feature in a Map model.", e);
- }
+ /**
+ * Indicate that the map widget view should navigate to a given target.
+ * This is accomplished by setting the zoom target on the MapInteraction
+ * model. The map widget listens to this change and updates the camera
+ * position accordingly.
+ * @param {Feature|MapAsset|GeoBoundingBox|Object} target The target to
+ * zoom to. See {@link CesiumWidgetView#flyTo} for more details on types
+ * of targets.
+ */
+ zoomTo: function (target) {
+ this.get("interactions")?.set("zoomTarget", target);
},
/**
- * Convert an array of feature objects to an array of Feature models.
- * @param {Cesium.Entity|Cesium.Cesium3DTileFeature|Feature[]} features - An
- * array of feature objects selected directly from the map view, or
- * @returns {Feature[]} An array of Feature models.
- * @since 2.25.0
+ * Indicate that the map widget view should navigate to the home position.
*/
- convertFeatures: function (features) {
- const model = this;
- const attrs = features.map(function (feature) {
- if (!feature) return null;
- if (feature instanceof Feature) return feature.attributes;
- // if this is already an object with feature attributes, return it
- if (
- feature.hasOwnProperty("mapAsset") &&
- feature.hasOwnProperty("properties")
- ) {
- return feature;
- }
- // Otherwise, assume it's a Cesium object and get the feature attributes
- return model.get("layers").getFeatureAttributes(features)?.[0];
- });
- return attrs.map((attr) => new Feature(attr));
+ flyHome: function () {
+ this.zoomTo(this.get("homePosition"));
},
/**
@@ -351,16 +282,30 @@ define([
* Add a layer or other asset to the map. This is the best way to add a
* layer to the map because it will ensure that this map model is set on
* the layer model.
- * @param {Object | MapAsset} layer - A map asset model or object with
+ * @todo Enable adding a terrain asset.
+ * @param {Object | MapAsset} asset - A map asset model or object with
* attributes to set on a new map asset model.
* @returns {MapAsset} The new layer model.
* @since 2.25.0
*/
- addAsset: function (layer) {
+ addAsset: function (asset) {
const layers = this.get("layers") || this.resetLayers();
- return layers.addAsset(layer, this);
+ return layers.addAsset(asset, this);
},
+ /**
+ * Remove a layer from the map.
+ * @param {MapAsset} asset - The layer model to remove from the map.
+ * @since x.x.x
+ */
+ removeAsset: function (asset) {
+ if (!asset) return;
+ const layers = this.get("layers");
+ if (!layers) return;
+ // Remove by ID because the model is passed directly. Not sure if this
+ // is a bug in the MapAssets collection or Backbone?
+ if (layers) layers.remove(asset.cid);
+ },
}
);
diff --git a/src/js/models/maps/MapInteraction.js b/src/js/models/maps/MapInteraction.js
new file mode 100644
index 000000000..96c37726e
--- /dev/null
+++ b/src/js/models/maps/MapInteraction.js
@@ -0,0 +1,354 @@
+"use strict";
+
+define([
+ "backbone",
+ "collections/maps/Features",
+ "models/maps/Feature",
+ "models/maps/GeoBoundingBox",
+ "models/maps/GeoPoint",
+ "models/maps/GeoScale",
+], function (Backbone, Features, Feature, GeoBoundingBox, GeoPoint, GeoScale) {
+ /**
+ * @class MapInteraction
+ * @classdesc The Map Interaction stores information about user interaction
+ * with a map, including the current position of the mouse, the feature that
+ * the mouse is currently hovering over, and the position on the map that the
+ * user has clicked, as well as the current view extent of the map.
+ * @classcategory Models/Maps
+ * @name MapInteraction
+ * @since x.x.x
+ * @extends Backbone.Model
+ */
+ var MapInteraction = Backbone.Model.extend(
+ /** @lends MapInteraction.prototype */ {
+ /**
+ * The type of model this is.
+ * @type {String}
+ */
+ type: "MapInteraction",
+
+ /**
+ * Overrides the default Backbone.Model.defaults() function to specify
+ * default attributes for the Map.
+ * @returns {Object} The default attributes for the Map.
+ * @property {GeoPoint} mousePosition - The current position of the mouse
+ * on the map. Updated by the map widget to show the longitude, latitude,
+ * and height (elevation) at the position of the mouse on the map.
+ * @property {GeoPoint} clickedPosition - The position on the map that the
+ * user last clicked.
+ * @property {GeoScale} scale - The current scale of the map in
+ * pixels:meters, i.e. The number of pixels on the screen that equal the
+ * number of meters on the map/globe. Updated by the map widget.
+ * @property {GeoBoundingBox} viewExtent - The extent of the currently
+ * visible area in the map widget. Updated by the map widget.
+ * @property {Features} hoveredFeatures - The feature that the mouse is
+ * currently hovering over. Updated by the map widget with a Feature model
+ * when a user hovers over a geographical feature on the map.
+ * @property {Features} clickedFeatures - The feature that the user last
+ * clicked. Updated by the map widget with a Feature model when a user
+ * clicks on a geographical feature on the map.
+ * @property {Features} selectedFeatures - Features from one or more
+ * layers that are highlighted or selected on the map. Updated by the map
+ * widget with a Feature model when a user selects a geographical feature
+ * on the map (e.g. by clicking)
+ * @property {Boolean} firstInteraction - Whether or not the user has
+ * interacted with the map yet. This is set to true when the user has
+ * clicked, hovered, panned, or zoomed the map. The only action that is
+ * ignored is mouse movement over the map.
+ * @property {String} previousAction - The previous action that was
+ * performed on the map. This may be any of the labels in the Cesium
+ * ScreenSpaceEventType enumeration:
+ * {@link https://cesium.com/learn/cesiumjs/ref-doc/global.html#ScreenSpaceEventType}
+ * @property {Feature|MapAsset|GeoBoundingBox} zoomTarget - The feature or
+ * map asset that the map should zoom to. The map widget should listen to
+ * this property and zoom to the specified feature or map asset when this
+ * property is set. The property should be cleared after the map widget
+ * has zoomed to the specified feature or map asset.
+ */
+ defaults: function () {
+ return {
+ mousePosition: new GeoPoint(),
+ clickedPosition: new GeoPoint(),
+ scale: new GeoScale(),
+ viewExtent: new GeoBoundingBox(),
+ hoveredFeatures: new Features(),
+ clickedFeatures: new Features(),
+ selectedFeatures: new Features(),
+ firstInteraction: false,
+ previousAction: null,
+ zoomTarget: null,
+ };
+ },
+
+ /**
+ * Run when a new Map is created.
+ * @param {MapConfig} attrs - An object specifying configuration options
+ * for the map. If any config option is not specified, the default will be
+ * used instead (see {@link MapInteraction#defaults}).
+ */
+ initialize: function (attrs, options) {
+ try {
+ this.connectEvents();
+ } catch (e) {
+ console.log("Error initializing a Map Interaction model", e);
+ }
+ },
+
+ /**
+ * Connects the MapInteraction model to events from the map widget.
+ */
+ connectEvents: function () {
+ this.listenForFirstInteraction();
+ this.listenForMoveStartAndChange();
+ this.listenTo(this, "change:previousAction", this.handleClick);
+ },
+
+ /**
+ * Listens for the first interaction with the map (click, hover, pan, or
+ * zoom) and sets the 'firstInteraction' attribute to true when it occurs.
+ */
+ listenForFirstInteraction: function () {
+ const model = this;
+ if (model.get("firstInteraction")) return;
+ const listener = new Backbone.Model();
+ listener.listenTo(
+ this,
+ "change:previousAction",
+ function (m, eventType) {
+ if (eventType != "MOUSE_MOVE") {
+ model.set("firstInteraction", true);
+ listener.stopListening();
+ listener.destroy();
+ }
+ }
+ );
+ },
+
+ /**
+ * Expands the camera events that are passed to the MapInteraction model
+ * from the map widget by creating a 'moveStartAndChanged' event. This
+ * event is triggered after the camera starts moving if and only if the
+ * camera position changes enough to trigger a 'cameraChanged' event. This
+ * event is useful for triggering actions that should only occur after the
+ * camera has moved and the camera position has changed.
+ * @since x.x.x
+ */
+ listenForMoveStartAndChange: function () {
+ if (this.moveStartChangeListener) {
+ this.moveStartChangeListener.destroy();
+ }
+ const listener = new Backbone.Model();
+ const model = this;
+ this.moveStartChangeListener = listener;
+ listener.stopListening(model, "moveStart");
+ listener.listenTo(model, "moveStart", function () {
+ listener.listenToOnce(model, "cameraChanged", function () {
+ listener.stopListening(model, "moveEnd");
+ model.trigger("moveStartAndChanged");
+ })
+ listener.listenToOnce(model, "moveEnd", function () {
+ listener.stopListening(model, "cameraChanged");
+ })
+ })
+ },
+
+ /**
+ * Handles a mouse click on the map. If the user has clicked on a feature,
+ * the feature is set as the 'clickedFeatures' attribute. If the map is
+ * configured to show details when a feature is clicked, the feature is
+ * also set as the 'selectedFeatures' attribute.
+ * @param {MapInteraction} m - The MapInteraction model.
+ * @param {String} action - The type of mouse click event that occurred.
+ * All except LEFT_CLICK are ignored.
+ */
+ handleClick: function (m, action) {
+ if (action !== "LEFT_CLICK") return;
+ // Clone the models in hovered features and set them as clicked features
+ const hoveredFeatures = this.get("hoveredFeatures").models;
+ this.setClickedFeatures(hoveredFeatures);
+ const clickAction = this.get("mapModel")?.get("clickFeatureAction");
+ if (clickAction === "showDetails") {
+ this.selectFeatures(hoveredFeatures);
+ } else if (clickAction === "zoom") {
+ this.set("zoomTarget", hoveredFeatures[0]);
+ }
+ // TODO: throttle this?
+ this.setClickedPositionFromMousePosition();
+ },
+
+ /**
+ * Sets the clicked position to the current mouse position.
+ */
+ setClickedPositionFromMousePosition: function () {
+ const mousePosition = this.get("mousePosition");
+ const coords = {
+ longitude: mousePosition.get("longitude"),
+ latitude: mousePosition.get("latitude"),
+ height: mousePosition.get("height"),
+ mapWidgetCoords: mousePosition.get("mapWidgetCoords"),
+ };
+ this.setClickedPosition(coords);
+ },
+
+ /**
+ * Set the position for either the mousePosition or clickedPosition
+ * attribute. Creates a new GeoPoint model if one doesn't already exist
+ * on the attribute.
+ * @param {'mousePosition'|'clickedPosition'} attributeName - The name of
+ * the attribute to set.
+ * @param {Object} position - An object with 'longitude' and 'latitude'
+ * properties.
+ * @returns {GeoPoint} The corresponding position as a GeoPoint model.
+ */
+ setPosition: function(attributeName, position) {
+ let point = this.get(attributeName);
+ if (!point) {
+ point = new GeoPoint();
+ this.set(attributeName, point);
+ }
+ point.set(position);
+ return point;
+ },
+
+ /**
+ * Sets the position on the map that the user last clicked.
+ * @param {Object} position - An object with 'longitude' and 'latitude'
+ * properties.
+ * @returns {GeoPoint} The clicked position as a GeoPoint model.
+ */
+ setClickedPosition: function(position) {
+ return this.setPosition("clickedPosition", position);
+ },
+
+ /**
+ * Sets the position of the mouse on the map.
+ * @param {Object} position - An object with 'longitude' and 'latitude'
+ * properties.
+ * @returns {GeoPoint} The mouse position as a GeoPoint model.
+ */
+ setMousePosition: function(position) {
+ return this.setPosition("mousePosition", position);
+ },
+
+ /**
+ * Set the pixel:meter scale of the map. Creates a new GeoScale model if
+ * one doesn't already exist on the scale attribute.
+ * @param {Object} scale - An object with 'meters' and 'pixels'
+ * properties.
+ * @returns {GeoScale} The scale as a GeoScale model.
+ */
+ setScale: function (scale) {
+ let scaleModel = this.get("scale");
+ if (!scaleModel) {
+ scaleModel = new GeoScale();
+ this.set("scale", scaleModel);
+ }
+ scaleModel.set(scale);
+ return scaleModel;
+ },
+
+ /**
+ * Set the extent of the map view. Creates a new GeoBoundingBox model if
+ * one doesn't already exist on the viewExtent attribute.
+ * @param {Object} extent - An object with 'north', 'east', 'south', and
+ * 'west' properties.
+ * @returns {GeoBoundingBox} The view extent as a GeoBoundingBox model.
+ */
+ setViewExtent: function (extent) {
+ let viewExtent = this.get("viewExtent");
+ if (!viewExtent) {
+ viewExtent = new GeoBoundingBox();
+ this.set("viewExtent", viewExtent);
+ }
+ viewExtent.set(extent);
+ return viewExtent;
+ },
+
+ /**
+ * Set the feature that the mouse is currently hovering over.
+ * @param {Cesium.Entity|Cesium.Cesium3DTileFeature|Feature[]} features -
+ * An array of feature objects selected directly from the map view.
+ */
+ setHoveredFeatures: function (features) {
+ this.setFeatures(features, "hoveredFeatures", true);
+ },
+
+ /**
+ * Set the feature that the user last clicked.
+ * @param {Cesium.Entity|Cesium.Cesium3DTileFeature|Feature[]|Object[]}
+ * features - An array of feature objects selected directly from the map
+ * view.
+ */
+ setClickedFeatures: function (features) {
+ this.setFeatures(features, "clickedFeatures", true);
+ },
+
+ /**
+ * Set the feature that is currently selected.
+ * @param {Cesium.Entity|Cesium.Cesium3DTileFeature|Feature[|Object[]]}
+ * features - An array of feature objects selected directly from the map
+ * view.
+ */
+ selectFeatures: function (features) {
+ const model = this;
+ this.setFeatures(features, "selectedFeatures", true);
+ },
+
+ /**
+ * Set features on either the hoveredFeatures, clickedFeatures, or
+ * selectedFeatures attribute. If the replace parameter is true, then the
+ * features will replace the current features on the attribute.
+ * @param {Cesium.Entity|Cesium.Cesium3DTileFeature|Feature[]|Object[]}
+ * features - An array of feature objects selected directly from the map
+ * view.
+ * @param {'hoveredFeatures'|'clickedFeatures'|'selectedFeatures'} type -
+ * The type of feature to set.
+ * @param {Boolean} [replace=true] - Whether or not to replace the current
+ * features on the attribute with the new features.
+ */
+ setFeatures: function (features, type, replace = true) {
+ try {
+ const model = this;
+
+ // Create a features collection if one doesn't already exist
+ if (!model.get(type)) model.set(type, new Features());
+
+ // Remove any null, undefined, or empty features
+ if (Array.isArray(features)) features = features.filter((f) => f);
+ if (features instanceof Features) {
+ features = features.filter((f) => !f.isDefault());
+ }
+
+ // Empty collection if features array is empty (and replace is true)
+ if (!features || features.length === 0) {
+ if (replace) model.get(type).set([], { remove: true });
+ return;
+ }
+
+ // Ignore if new features are identical to the current features
+ const currentFeatures = model.get(type);
+ if (
+ features &&
+ currentFeatures &&
+ currentFeatures.length === features.length &&
+ currentFeatures.containsFeatures(features)
+ ) {
+ return;
+ }
+
+ const assets = this.get("mapModel")?.get("layers");
+
+ const newAttrs = features.map((f) => ({ featureObject: f }));
+
+ model
+ .get(type)
+ .set(newAttrs, { remove: replace, parse: true, assets: assets });
+ } catch (e) {
+ console.log("Failed to select a Feature in a Map model.", e);
+ }
+ },
+ }
+ );
+
+ return MapInteraction;
+});
diff --git a/src/js/models/maps/assets/Cesium3DTileset.js b/src/js/models/maps/assets/Cesium3DTileset.js
index 0968dda8e..df560048a 100644
--- a/src/js/models/maps/assets/Cesium3DTileset.js
+++ b/src/js/models/maps/assets/Cesium3DTileset.js
@@ -2,22 +2,14 @@
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
) {
@@ -74,7 +66,8 @@ define(
* specific to each type of asset.
*/
defaults: function () {
- return _.extend(
+ return Object.assign(
+ {},
this.constructor.__super__.defaults(),
{
type: 'Cesium3DTileset',
@@ -215,6 +208,9 @@ define(
setListeners: function () {
try {
+ // call the super method
+ this.constructor.__super__.setListeners.call(this);
+
// When opacity, color, or visibility changes (will also update the filters)
this.stopListening(this, 'change:opacity change:color change:visible')
this.listenTo(
@@ -245,6 +241,8 @@ define(
// changes on a Cesium map.
const cesiumModel = model.get('cesiumModel')
+ if(!cesiumModel) return
+
// If the layer isn't visible at all, don't bother setting up colors or
// filters. Just set every feature to hidden.
if (!model.isVisible()) {
diff --git a/src/js/models/maps/assets/CesiumGeohash.js b/src/js/models/maps/assets/CesiumGeohash.js
index c07e8e72c..ed4c0a4b6 100644
--- a/src/js/models/maps/assets/CesiumGeohash.js
+++ b/src/js/models/maps/assets/CesiumGeohash.js
@@ -153,9 +153,8 @@ define([
getPrecision: function () {
const limit = this.get("maxGeoHashes");
const geohashes = this.get("geohashes")
- const bounds = this.get("mapModel").get("currentViewExtent");
- const area = geohashes.getBoundingBoxArea(bounds);
- return this.get("geohashes").getMaxPrecision(area, limit);
+ const area = this.getViewExtent().getArea();
+ return geohashes.getMaxPrecision(area, limit);
},
/**
@@ -220,12 +219,19 @@ define([
* @since 2.25.0
*/
getGeohashesForExtent: function () {
- const extent = this.get("mapModel")?.get("currentViewExtent");
- const bounds = Object.assign({}, extent);
- delete bounds.height;
+ const bounds = this.getViewExtent()?.clone();
+ if (!bounds) return this.get("geohashes");
return this.get("geohashes")?.getSubsetByBounds(bounds);
},
+ /**
+ * Get the current map extent from the Map Interaction model.
+ * @returns {GeoBoundingBox} The current map extent
+ */
+ getViewExtent: function () {
+ return this.get("mapModel")?.get("interactions")?.get("viewExtent")
+ },
+
/**
* Returns the GeoJSON representation of the geohashes.
* @param {Boolean} [limitToExtent = true] - Set to false to return the
@@ -271,11 +277,11 @@ define([
// Set the GeoJSON representing geohashes on the model
const cesiumOptions = this.getCesiumOptions();
const type = model.get("type");
- const data = type === "geojson" ? this.getGeoJSON() : this.getCZML();
+ const data = type === "GeoJsonDataSource" ? this.getGeoJSON() : this.getCZML();
cesiumOptions["data"] = data;
cesiumOptions["height"] = 0;
model.set("cesiumOptions", cesiumOptions);
- // Create the model like a regular GeoJSON data source
+ // Create the model like a regular vector data source
CesiumVectorData.prototype.createCesiumModel.call(this, recreate);
} catch (e) {
console.log("Error creating a CesiumGeohash model. ", e);
@@ -289,19 +295,23 @@ define([
* @since 2.25.0
*/
selectGeohashes: function (geohashes) {
- const toSelect = [...new Set(geohashes.map((geohash) => {
- const parent = this.get("geohashes").getContainingGeohash(geohash);
- return parent?.get("hashString");
- }, this))];
- const entities = this.get("cesiumModel").entities.values;
- const selected = entities.filter((entity) => {
- const hashString = this.getPropertiesFromFeature(entity).hashString;
- return toSelect.includes(hashString);
- });
- const featureAttrs = selected.map((feature) => {
- return this.getFeatureAttributes(feature);
- });
- this.get("mapModel").selectFeatures(featureAttrs);
+ try {
+ const toSelect = [...new Set(geohashes.map((geohash) => {
+ const parent = this.get("geohashes").getContainingGeohash(geohash);
+ return parent?.get("hashString");
+ }, this))];
+ const entities = this.get("cesiumModel").entities.values;
+ const selected = entities.filter((entity) => {
+ const hashString = this.getPropertiesFromFeature(entity).hashString;
+ return toSelect.includes(hashString);
+ });
+ const featureAttrs = selected.map((feature) => {
+ return this.getFeatureAttributes(feature);
+ });
+ this.get("mapModel").selectFeatures(featureAttrs);
+ } catch (e) {
+ console.log("Error selecting geohashes", e);
+ }
},
}
);
diff --git a/src/js/models/maps/assets/CesiumVectorData.js b/src/js/models/maps/assets/CesiumVectorData.js
index fee95fc5b..cecfe759a 100644
--- a/src/js/models/maps/assets/CesiumVectorData.js
+++ b/src/js/models/maps/assets/CesiumVectorData.js
@@ -1,667 +1,788 @@
-'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: The GeoJsonDataSource and CzmlDataSource are the only supported
- * DataSources so far, but eventually this model could be used to support
- * the 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' or 'CzmlDataSource'.
- * @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.
- * @property {outlineColor} [outlineColor=null] The color of the outline of the
- * features. If null, the outline will not be shown. If a string, it should be a
- * valid CSS color string. If an object, it should be an AssetColor object, or
- * a set of RGBA values.
- */
- defaults: function () {
- return Object.assign(
- this.constructor.__super__.defaults(),
- {
- type: 'GeoJsonDataSource',
- filters: new VectorFilters(),
- cesiumModel: null,
- cesiumOptions: {},
- colorPalette: new AssetColorPalette(),
- icon: '',
- outlineColor: null,
- featureType: Cesium.Entity
- }
- );
- },
-
- /**
- * 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 {
-
- if(!assetConfig) assetConfig = {};
+"use strict";
+
+define([
+ "underscore",
+ "cesium",
+ "models/maps/assets/MapAsset",
+ "models/maps/AssetColor",
+ "models/maps/AssetColorPalette",
+ "collections/maps/VectorFilters",
+], function (
+ _,
+ 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, CzmlDataSource, and CustomDataSource are
+ * supported. Eventually this model could support the KmlDataSource.
+ * @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' or 'CzmlDataSource'.
+ * @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.
+ * @property {string|AssetColor} [outlineColor=null] The color of the
+ * outline of the features. If null, the outline will not be shown. If a
+ * string, it should be a valid CSS color string. If an object, it should
+ * be an AssetColor object, or a set of RGBA values.
+ */
+ defaults: function () {
+ return Object.assign(this.constructor.__super__.defaults(), {
+ type: "GeoJsonDataSource",
+ filters: new VectorFilters(),
+ cesiumModel: null,
+ cesiumOptions: {},
+ colorPalette: new AssetColorPalette(),
+ icon: '',
+ outlineColor: null,
+ featureType: Cesium.Entity,
+ });
+ },
+
+ /**
+ * 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 {
+ if (!assetConfig) assetConfig = {};
+
+ MapAsset.prototype.initialize.call(this, assetConfig);
+
+ if (assetConfig.filters) {
+ this.set("filters", new VectorFilters(assetConfig.filters));
+ }
- MapAsset.prototype.initialize.call(this, assetConfig);
+ // 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);
+
+ if (
+ assetConfig.outlineColor &&
+ !(assetConfig.outlineColor instanceof AssetColor)
+ ) {
+ this.set(
+ "outlineColor",
+ new AssetColor({ color: assetConfig.outlineColor })
+ );
+ }
- if (assetConfig.filters) {
- this.set('filters', new VectorFilters(assetConfig.filters))
- }
+ if (
+ assetConfig.highlightColor &&
+ !(assetConfig.highlightColor instanceof AssetColor)
+ ) {
+ this.set(
+ "highlightColor",
+ new AssetColor({ color: assetConfig.highlightColor })
+ );
+ }
- // 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)
-
- if (
- assetConfig.outlineColor &&
- !(assetConfig.outlineColor instanceof AssetColor)
- ) {
- this.set(
- "outlineColor",
- new AssetColor({ color: assetConfig.outlineColor })
- );
- }
-
- if (
- assetConfig.highlightColor &&
- !(assetConfig.highlightColor instanceof AssetColor)
- ) {
- this.set(
- "highlightColor",
- new AssetColor({ color: assetConfig.highlightColor })
- );
+ this.createCesiumModel();
+ } catch (error) {
+ console.log("Error initializing a CesiumVectorData model.", 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 = false] - 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 = this.getCesiumOptions();
+ 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
+ let dataSource = model.get("cesiumModel");
+ if (dataSource) {
+ if (!recreate) {
+ return dataSource;
+ } else {
+ // If we are recreating the model, remove all entities first. see
+ // https://stackoverflow.com/questions/31426796/loading-updated-data-with-geojsondatasource-in-cesium-js
+ dataSource.entities.removeAll();
}
+ }
- this.createCesiumModel();
+ model.set("displayReady", false);
+ model.resetStatus();
+ if (typeof dataSourceFunction !== "function") {
+ model.setError(`${type} is not a supported data type.`);
+ return;
}
- catch (error) {
- console.log('Error initializing a CesiumVectorData model.', error);
+ if (!dataSource) {
+ dataSource = new dataSourceFunction(label);
+ }
+ if (!dataSource) {
+ model.setError("Failed to create a Cesium DataSource model.");
+ return;
}
- },
-
- /**
- * 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 = false] - 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 = this.getCesiumOptions();
- 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
- let dataSource = model.get('cesiumModel')
- if (dataSource) {
- if (!recreate) {
- return dataSource
- } else {
- // If we are recreating the model, remove all entities first.
- // see https://stackoverflow.com/questions/31426796/loading-updated-data-with-geojsondatasource-in-cesium-js
- dataSource.entities.removeAll();
- // Make sure the CesiumWidgetView re-renders the data
- model.set('displayReady', false);
- }
- }
-
- model.resetStatus();
- if (!cesiumOptions || !cesiumOptions.data) {
- model.set('status', 'error');
- model.set('statusDetails', 'Vector data source is missing: A URL or data object is required')
- return
- }
+ // There is no data to load for a CustomDataSource
+ if (type === "CustomDataSource") {
+ model.set("cesiumModel", dataSource);
+ model.setListeners();
+ model.setReady();
+ model.runVisualizers();
+ return;
+ }
- if (dataSourceFunction && typeof dataSourceFunction === 'function') {
+ // For GeoJSON and CZML data sources
+ if (!cesiumOptions || !cesiumOptions.data) {
+ model.setError(
+ "No data was provided to create a Cesium DataSource model."
+ );
+ return;
+ }
+ const data = JSON.parse(JSON.stringify(cesiumOptions.data));
+ delete cesiumOptions.data;
+ dataSource
+ .load(data, cesiumOptions)
+ .then(function (loadedData) {
+ model.set("cesiumModel", loadedData);
if (!recreate) {
- dataSource = new dataSourceFunction(label)
+ model.setListeners();
}
-
- const data = cesiumOptions.data;
- delete cesiumOptions.data
-
- dataSource.load(data, cesiumOptions)
- .then(function (loadedData) {
- model.set('cesiumModel', loadedData)
- if (!recreate) {
- 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)
- })
+ model.updateFeatureVisibility();
+ model.updateAppearance();
+ model.setReady();
+ })
+ .otherwise(model.setError.bind(model));
+ } catch (error) {
+ console.log("Failed to create a VectorData Cesium Model.", error);
+ }
+ },
+
+ /**
+ * Set listeners that update the cesium model when the backbone model is
+ * updated.
+ */
+ setListeners: function () {
+ try {
+ MapAsset.prototype.setListeners.call(this);
+ const appearEvents =
+ "change:visible change:opacity change:color change:outlineColor" +
+ " change:temporarilyHidden";
+ this.stopListening(this, appearEvents);
+ this.listenTo(this, appearEvents, this.updateAppearance);
+ const filters = this.get("filters");
+ this.stopListening(filters, "update");
+ this.listenTo(filters, "update", this.updateFeatureVisibility);
+ } catch (error) {
+ console.log("Failed to set CesiumVectorData listeners.", 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);
+ }
+ });
+ });
+ });
+ },
+
+ /**
+ * Try to find Entity object that comes from an object passed from the
+ * Cesium map. This is useful when the map is clicked and the map returns
+ * an object that may or may not be an Entity.
+ * @param {Object} mapObject - An object returned from the Cesium map
+ * @returns {Cesium.Entity} - The Entity object if found, otherwise null.
+ * @since 2.25.0
+ */
+ getEntityFromMapObject: function (mapObject) {
+ const entityType = this.get("featureType");
+ if (mapObject instanceof entityType) return mapObject;
+ if (mapObject.id instanceof entityType) return mapObject.id;
+ return null;
+ },
+
+ /**
+ * @inheritdoc
+ * @since 2.25.0
+ */
+ getFeatureAttributes: function (feature) {
+ feature = this.getEntityFromMapObject(feature);
+ return MapAsset.prototype.getFeatureAttributes.call(this, feature);
+ },
+
+ /**
+ * @inheritdoc
+ * @since 2.25.0
+ */
+ usesFeatureType: function (feature) {
+ // This method could be passed the entity directly, or the object
+ // returned from Cesium on a click event (where the entity is in the id
+ // property).
+ if (!feature) return false;
+ const baseMethod = MapAsset.prototype.usesFeatureType;
+ let result = baseMethod.call(this, feature);
+ if (result) return result;
+ result = baseMethod.call(this, feature.id);
+ return result;
+ },
+
+ /**
+ * 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: function (feature) {
+ feature = this.getEntityFromMapObject(feature);
+ if (!feature) return null;
+ const featureProps = feature.properties;
+ let properties = {};
+ if (featureProps) {
+ properties = feature.properties.getValue(new Date());
+ }
+ properties = this.addCustomProperties(properties);
+ return properties;
+ },
+
+ /**
+ * Return the label for a feature from a DataSource model
+ * @param {Cesium.Entity} feature A Cesium Entity
+ * @returns {string} The label
+ */
+ getLabelFromFeature: function (feature) {
+ feature = this.getEntityFromMapObject(feature);
+ if (!feature) return null;
+ return feature.name;
+ },
+
+ /**
+ * Return the DataSource model for a feature from a Cesium DataSource
+ * model
+ * @param {Cesium.Entity} feature A Cesium Entity
+ * @returns {Cesium.GeoJsonDataSource|Cesium.CzmlDataSource} The model
+ */
+ getCesiumModelFromFeature: function (feature) {
+ feature = this.getEntityFromMapObject(feature);
+ if (!feature) return null;
+ return feature.entityCollection.owner;
+ },
+
+ /**
+ * Return the ID used by Cesium for a feature from a DataSource model
+ * @param {Cesium.Entity} feature A Cesium Entity
+ * @returns {string} The ID
+ */
+ getIDFromFeature: function (feature) {
+ feature = this.getEntityFromMapObject(feature);
+ if (!feature) return null;
+ return feature.id;
+ },
+
+ /**
+ * Updates the styles set on the cesiumModel object based on the
+ * colorPalette and filters attributes.
+ */
+ updateAppearance: function () {
+ try {
+ const model = this;
+ const entities = this.getEntities();
+ const entityCollection = this.getEntityCollection();
+ this.set("displayReady", false);
+
+ if (entities && entities.length) {
+ if (model.isVisible()) {
+ // Suspending events while updating a large number of entities helps
+ // performance.
+ model.suspendEvents();
+ entityCollection.show = true;
+ this.styleEntities(entities);
+ model.resumeEvents();
} else {
- model.set('status', 'error')
- model.set('statusDetails', type + ' is not a supported imagery type.')
+ // If the asset isn't visible, just hide all entities and update the
+ // visibility property to indicate that layer is hidden
+ entityCollection.show = false;
+ if (model.get("opacity") === 0) model.set("visible", false);
}
}
- catch (error) {
- console.log(
- 'Failed to create a Cesium Model for a CesiumVectorData model' +
- '. Error details: ' + error
- );
+
+ this.runVisualizers();
+ } catch (e) {
+ console.log("Failed to update CesiumVectorData model styles.", e);
+ }
+ },
+
+ /**
+ * Run the Cesium visualizers for this asset. Visualizers render data
+ * associated with DataSource instances. Visualizers must be run after
+ * changes are made to the data or the appearance of the data.
+ * @since x.x.x
+ * @see {@link https://cesium.com/learn/cesiumjs/ref-doc/Visualizer.html}
+ */
+ runVisualizers: function () {
+ const dataSource = this.get("cesiumModel");
+ const visualizers = dataSource?._visualizers;
+ if (!visualizers || !visualizers.length) {
+ this.whenVisualizersReady(this.runVisualizers.bind(this));
+ return;
+ }
+ const time = Cesium.JulianDate.now();
+ let displayReadyNow = true
+ for (let x = 0; x < visualizers.length; x++) {
+ displayReadyNow = visualizers[x].update(time) && displayReadyNow;
+ }
+ if (!displayReadyNow) {
+ setTimeout(this.runVisualizers.bind(this), 300);
+ } else {
+ this.set("displayReady", true);
+ }
+ this.trigger("appearanceChanged");
+ },
+
+ /**
+ * Check for the existence of visualizers and run the callback when they
+ * are ready. This is useful for waiting to run code that depends on the
+ * visualizers being ready. It will attempt to run the callback every
+ * pingRate ms until the visualizers are ready, or until the maxPings is
+ * reached.
+ * @param {Function} callBack - The function to run when the visualizers
+ * are ready
+ * @param {Number} [pingRate=100] - The number of milliseconds to wait
+ * between pings - pings are used to check if the visualizers are ready
+ * @param {Number} [maxPings=30] - The maximum number of pings to wait
+ * before giving up
+ */
+ whenVisualizersReady: function (callBack, pingRate = 100, maxPings = 30) {
+ const model = this;
+ let pings = 0;
+ const interval = setInterval(function () {
+ pings++;
+ if (pings > maxPings) {
+ clearInterval(interval);
+ return;
}
- },
-
- /**
- * Set listeners that update the cesium model when the backbone model is updated.
- */
- setListeners: function () {
- try {
- const appearEvents =
- 'change:visible change:opacity change:color change:outlineColor' +
- ' change:temporarilyHidden'
- this.stopListening(this, appearEvents)
- this.listenTo(this, appearEvents, this.updateAppearance)
- this.stopListening(this.get('filters'), 'update')
- this.listenTo(this.get('filters'), 'update', this.updateFeatureVisibility)
+ const visualizers = model.get("cesiumModel")?._visualizers;
+ if (visualizers && visualizers.length) {
+ clearInterval(interval);
+ callBack();
}
- catch (error) {
- console.log(
- 'There was an error setting listeners in a CesiumVectorData model' +
- '. Error details: ' + error
- );
+ }, pingRate);
+ },
+
+ /**
+ * Get the Cesium EntityCollection for this asset
+ * @returns {Cesium.EntityCollection} The Cesium EntityCollection
+ * @since x.x.x
+ */
+ getEntityCollection: function () {
+ const model = this;
+ const dataSource = model.get("cesiumModel");
+ return dataSource?.entities;
+ },
+
+ /**
+ * Get the Cesium Entities for this asset
+ * @returns {Cesium.Entity[]} The Cesium Entities
+ * @since x.x.x
+ */
+ getEntities: function () {
+ return this.getEntityCollection()?.values || [];
+ },
+
+ /**
+ * Suspend events on the Cesium EntityCollection. This will prevent
+ * visualizers from running until resumeEvents is called.
+ * @since x.x.x
+ */
+ suspendEvents: function () {
+ const entities = this.getEntityCollection();
+ if (entities) entities.suspendEvents();
+ },
+
+ /**
+ * Resume events on the Cesium EntityCollection. This will allow
+ * visualizers to run again.
+ * @since x.x.x
+ */
+ resumeEvents: function () {
+ const entities = this.getEntityCollection();
+ if (entities) entities.resumeEvents();
+ },
+
+ /**
+ * Manually an entity to the Cesium EntityCollection.
+ * @param {Object} entity - The ConstructorOptions with properties to pass
+ * to Cesium.EntityCollection.add. See
+ * {@link https://cesium.com/learn/cesiumjs/ref-doc/EntityCollection.html?classFilter=EntityCollection#add}
+ * @returns {Cesium.Entity} The Cesium Entity that was added
+ * @since x.x.x
+ */
+ addEntity: function (entity) {
+ try {
+ const entities = this.getEntityCollection();
+ if (!entities) return false;
+ const newEntity = entities.add(entity);
+ this.styleEntities([newEntity]);
+ this.runVisualizers();
+ return newEntity;
+ } catch (e) {
+ console.log("Failed to add an entity.", e);
+ }
+ },
+
+ /**
+ * Manually remove an entity from the Cesium EntityCollection.
+ * @param {Cesium.Entity|string} entity - The entity or ID of the entity
+ * to remove
+ * @returns {Boolean} True if the entity was removed, false otherwise
+ * @since x.x.x
+ */
+ removeEntity: function (entity) {
+ try {
+ const entities = this.getEntityCollection();
+ if (!entities) return false;
+ let removed = false;
+ // if entity is a string, remove by ID
+ if (typeof entity === "string") {
+ removed = entities.removeById(entity);
+ } else {
+ // Otherwise, assume it's an entity object
+ removed = entities.remove(entity);
}
- },
-
- /**
- * 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)
- }
- })
- });
- })
- },
-
- /**
- * Try to find Entity object that comes from an object passed from the
- * Cesium map. This is useful when the map is clicked and the map
- * returns an object that may or may not be an Entity.
- * @param {Object} mapObject - An object returned from the Cesium map
- * @returns {Cesium.Entity} - The Entity object if found, otherwise null.
- * @since 2.25.0
- */
- getEntityFromMapObject(mapObject) {
- const entityType = this.get("featureType")
- if (mapObject instanceof entityType) return mapObject
- if (mapObject.id instanceof entityType) return mapObject.id
- return null
- },
-
- /**
- * @inheritdoc
- * @since 2.25.0
- */
- getFeatureAttributes: function (feature) {
- feature = this.getEntityFromMapObject(feature)
- return MapAsset.prototype.getFeatureAttributes.call(this, feature)
- },
-
- /**
- * @inheritdoc
- * @since 2.25.0
- */
- usesFeatureType: function (feature) {
- // This method could be passed the entity directly, or the object
- // returned from Cesium on a click event (where the entity is in the
- // id property).
- if(!feature) return false
- const baseMethod = MapAsset.prototype.usesFeatureType
- let result = baseMethod.call(this, feature)
- if (result) return result
- result = baseMethod.call(this, feature.id)
- return result
- },
-
- /**
- * 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) {
- feature = this.getEntityFromMapObject(feature)
- if (!feature) return null
- const featureProps = feature.properties
- let properties = {}
- if (featureProps) {
- properties = feature.properties.getValue(new Date())
+ this.runVisualizers();
+ return removed;
+ } catch (e) {
+ console.log("Failed to remove an entity.", e);
+ }
+ },
+
+ /**
+ * Update the styles for a set of entities
+ * @param {Array} entities - The entities to update
+ * @since 2.25.0
+ */
+ styleEntities: function (entities) {
+ // Map of entity types to style functions
+ const entityStyleMap = {
+ polygon: this.stylePolygon,
+ polyline: this.stylePolyline,
+ billboard: this.styleBillboard,
+ point: this.stylePoint,
+ };
+
+ entities.forEach((entity) => {
+ const styles = this.getStyles(entity);
+ if (!styles) {
+ entity.show = false;
+ return;
}
- properties = this.addCustomProperties(properties)
- return properties
- },
-
- /**
- * Return the label for a feature from a DataSource model
- * @param {Cesium.Entity} feature A Cesium Entity
- * @returns {string} The label
- */
- getLabelFromFeature: function (feature) {
- feature = this.getEntityFromMapObject(feature)
- if (!feature) return null
- return feature.name
- },
-
- /**
- * Return the DataSource model for a feature from a Cesium DataSource
- * model
- * @param {Cesium.Entity} feature A Cesium Entity
- * @returns {Cesium.GeoJsonDataSource|Cesium.CzmlDataSource} The model
- */
- getCesiumModelFromFeature: function (feature) {
- feature = this.getEntityFromMapObject(feature)
- if (!feature) return null
- return feature.entityCollection.owner
- },
-
- /**
- * Return the ID used by Cesium for a feature from a DataSource model
- * @param {Cesium.Entity} feature A Cesium Entity
- * @returns {string} The ID
- */
- getIDFromFeature: function (feature) {
- feature = this.getEntityFromMapObject(feature)
- if (!feature) return null
- return feature.id
- },
-
- /**
- * 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 entities = cesiumModel.entities.values
-
- // Suspending events while updating a large number of entities helps
- // performance.
- cesiumModel.entities.suspendEvents()
-
- // If the asset isn't visible, just hide all entities and update the
- // visibility property to indicate that layer is hidden
- if (!model.isVisible()) {
- cesiumModel.entities.show = false
- if (model.get('opacity') === 0) model.set('visible', false);
- } else {
- cesiumModel.entities.show = true
- this.styleEntities(entities)
+ entity.show = true;
+ for (const [type, styleFunction] of Object.entries(entityStyleMap)) {
+ if (entity[type]) {
+ styleFunction.call(this, entity, styles);
}
-
- 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 (e) {
- console.log('Failed to update CesiumVectorData model styles.', e);
- }
- },
-
- /**
- * Update the styles for a set of entities
- * @param {Array} entities - The entities to update
- * @since 2.25.0
- */
- styleEntities: function (entities) {
-
- // Map of entity types to style functions
- const entityStyleMap = {
- polygon: this.stylePolygon,
- polyline: this.stylePolyline,
- billboard: this.styleBillboard,
- point: this.stylePoint,
- };
-
- entities.forEach(entity => {
- const styles = this.getStyles(entity);
- if (!styles) {
- entity.show = false;
- return;
- }
- entity.show = true;
- for (const [type, styleFunction] of Object.entries(entityStyleMap)) {
- if (entity[type]) {
- styleFunction.call(this, entity, styles);
- }
+ }, this);
+ },
+
+ /**
+ * Update the styles for a polygon entity
+ * @param {Cesium.Entity} entity - The entity to update
+ * @param {Object} styles - Styles to apply, as returned by getStyles
+ * @since 2.25.0
+ */
+ stylePolygon: function (entity, styles) {
+ entity.polygon.material = styles.color;
+ entity.polygon.outline = styles.outline;
+ entity.polygon.outlineColor = styles.outlineColor;
+ entity.polygon.outlineWidth = styles.outline ? 2 : 0;
+ },
+
+ /**
+ * Update the styles for a point entity
+ * @param {Cesium.Entity} entity - The entity to update
+ * @param {Object} styles - Styles to apply, as returned by getStyles
+ * @since 2.25.0
+ */
+ stylePoint: function (entity, styles) {
+ entity.point.color = styles.color;
+ entity.point.outlineColor = styles.outlineColor;
+ entity.point.outlineWidth = styles.outline ? 2 : 0;
+ entity.point.pixelSize = styles.pointSize;
+ },
+
+ /**
+ * Update the styles for a polyline entity
+ * @param {Cesium.Entity} entity - The entity to update
+ * @param {Object} styles - Styles to apply, as returned by getStyles
+ * @since 2.25.0
+ */
+ stylePolyline: function (entity, styles) {
+ entity.polyline.material = styles.color;
+ entity.polyline.width = styles.lineWidth;
+ },
+
+ /**
+ * Update the styles for a billboard entity
+ * @param {Cesium.Entity} entity - The entity to update
+ * @param {Object} styles - Styles to apply, as returned by getStyles
+ * @since 2.25.0
+ */
+ styleBillboard: function (entity, styles) {
+ if (!this.pinBuilder) {
+ this.pinBuilder = new Cesium.PinBuilder();
+ }
+ entity.billboard.image = this.pinBuilder
+ .fromColor(styles.color, styles.markerSize)
+ .toDataURL();
+ // To convert the automatically created billboards to points instead:
+ // entity.billboard = undefined; entity.point = new
+ // Cesium.PointGraphics();
+ },
+
+ /**
+ * Update the styles for a label entity
+ * @param {Cesium.Entity} entity - The entity to update
+ * @param {Object} styles - Styles to apply, as returned by getStyles
+ * @since 2.25.0
+ */
+ styleLabel: function (entity, styles) {
+ // TODO...
+ },
+
+ /**
+ * Covert a Color model to a Cesium Color
+ * @param {Color} color A Color model
+ * @returns {Cesium.Color|null} A Cesium Color or null if the color is
+ * invalid
+ * @since 2.25.0
+ */
+ colorToCesiumColor: function (color) {
+ color = color?.get ? color.get("color") : color;
+ if (!color) return null;
+ return new Cesium.Color(
+ color.red,
+ color.green,
+ color.blue,
+ color.alpha
+ );
+ },
+
+ /**
+ * Return the color for a feature based on the colorPalette and filters
+ * attributes.
+ * @param {Cesium.Entity} entity A Cesium Entity
+ * @returns {Cesium.Color|null} A Cesium Color or null if the color is
+ * invalid or alpha is 0
+ * @since 2.25.0
+ */
+ colorForEntity: function (entity) {
+ const properties = this.getPropertiesFromFeature(entity);
+ const color = this.colorToCesiumColor(this.getColor(properties));
+ const alpha = color.alpha * this.get("opacity");
+ if (alpha === 0) return null;
+ color.alpha = alpha;
+ return this.colorToCesiumColor(color);
+ },
+
+ /**
+ * Return the styles for a selected feature
+ * @param {Cesium.Entity} entity A Cesium Entity
+ * @returns {Object} An object containing the styles for the feature
+ * @since 2.25.0
+ */
+ getSelectedStyles: function (entity) {
+ const highlightColor = this.colorToCesiumColor(
+ this.get("highlightColor")
+ );
+ return {
+ color: highlightColor || this.colorForEntity(entity),
+ outlineColor: Cesium.Color.WHITE,
+ outline: true,
+ lineWidth: 7,
+ markerSize: 34,
+ pointSize: 17,
+ };
+ },
+
+ /**
+ * Return the styles for a feature
+ * @param {Cesium.Entity} entity A Cesium Entity
+ * @returns {Object} An object containing the styles for the feature
+ * @since 2.25.0
+ */
+ getStyles: function (entity) {
+ if (!entity) return null;
+ entity = this.getEntityFromMapObject(entity);
+ if (this.featureIsSelected(entity)) {
+ return this.getSelectedStyles(entity);
+ }
+ const color = this.colorForEntity(entity);
+ if (!color) {
+ return null;
+ }
+ const outlineColor = this.colorToCesiumColor(
+ this.get("outlineColor")?.get("color")
+ );
+ return {
+ color: color,
+ outlineColor: outlineColor,
+ outline: outlineColor ? true : false,
+ lineWidth: 3,
+ markerSize: 25,
+ pointSize: 13,
+ };
+ },
+
+ /**
+ * Shows or hides each feature from this Map Asset based on the filters.
+ */
+ updateFeatureVisibility: function () {
+ try {
+ const model = this;
+ const entities = this.getEntities();
+ const filters = model.get("filters");
+
+ if (!entities || !filters) return;
+
+ // Suspending events while updating a large number of entities helps
+ // performance.
+ this.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);
}
- }, this);
- },
-
- /**
- * Update the styles for a polygon entity
- * @param {Cesium.Entity} entity - The entity to update
- * @param {Object} styles - Styles to apply, as returned by getStyles
- * @since 2.25.0
- */
- stylePolygon: function (entity, styles) {
- entity.polygon.material = styles.color
- entity.polygon.outline = styles.outline;
- entity.polygon.outlineColor = styles.outlineColor
- entity.polygon.outlineWidth = styles.outline ? 2 : 0
- },
-
- /**
- * Update the styles for a point entity
- * @param {Cesium.Entity} entity - The entity to update
- * @param {Object} styles - Styles to apply, as returned by getStyles
- * @since 2.25.0
- */
- stylePoint: function (entity, styles) {
- entity.point.color = styles.color
- entity.point.outlineColor = styles.outlineColor
- entity.point.outlineWidth = styles.outline ? 2 : 0
- entity.point.pixelSize = styles.pointSize
- },
-
- /**
- * Update the styles for a polyline entity
- * @param {Cesium.Entity} entity - The entity to update
- * @param {Object} styles - Styles to apply, as returned by getStyles
- * @since 2.25.0
- */
- stylePolyline: function (entity, styles) {
- entity.polyline.material = styles.color
- entity.polyline.width = styles.lineWidth
- },
-
- /**
- * Update the styles for a billboard entity
- * @param {Cesium.Entity} entity - The entity to update
- * @param {Object} styles - Styles to apply, as returned by getStyles
- * @since 2.25.0
- */
- styleBillboard: function (entity, styles) {
- if (!this.pinBuilder) {
- this.pinBuilder = new Cesium.PinBuilder()
+ entity.show = visible;
}
- entity.billboard.image = this.pinBuilder.fromColor(
- styles.color, styles.markerSize).toDataURL()
- // To convert the automatically created billboards to points instead:
- // entity.billboard = undefined;
- // entity.point = new Cesium.PointGraphics();
- },
-
- /**
- * Update the styles for a label entity
- * @param {Cesium.Entity} entity - The entity to update
- * @param {Object} styles - Styles to apply, as returned by getStyles
- * @since 2.25.0
- */
- styleLabel: function (entity, styles) {
- // TODO...
- },
-
- /**
- * Covert a Color model to a Cesium Color
- * @param {Color} color A Color model
- * @returns {Cesium.Color|null} A Cesium Color or null if the color is
- * invalid
- * @since 2.25.0
- */
- colorToCesiumColor: function (color) {
- color = color?.get ? color.get("color") : color;
- if(!color) return null
- return new Cesium.Color(
- color.red, color.green, color.blue, color.alpha
- )
- },
-
- /**
- * Return the color for a feature based on the colorPalette and filters
- * attributes.
- * @param {Cesium.Entity} entity A Cesium Entity
- * @returns {Cesium.Color|null} A Cesium Color or null if the color is
- * invalid or alpha is 0
- * @since 2.25.0
- */
- colorForEntity: function (entity) {
- const properties = this.getPropertiesFromFeature(entity);
- const color = this.colorToCesiumColor(this.getColor(properties));
- const alpha = color.alpha * this.get("opacity");
- if (alpha === 0) return null;
- color.alpha = alpha;
- return this.colorToCesiumColor(color);
- },
-
- /**
- * Return the styles for a selected feature
- * @param {Cesium.Entity} entity A Cesium Entity
- * @returns {Object} An object containing the styles for the feature
- * @since 2.25.0
- */
- getSelectedStyles: function (entity) {
- const highlightColor = this.colorToCesiumColor(
- this.get("highlightColor")
- );
- return {
- "color": highlightColor || this.colorForEntity(entity),
- "outlineColor": Cesium.Color.WHITE,
- "outline": true,
- "lineWidth": 7,
- "markerSize": 34,
- "pointSize": 17
- }
- },
-
- /**
- * Return the styles for a feature
- * @param {Cesium.Entity} entity A Cesium Entity
- * @returns {Object} An object containing the styles for the feature
- * @since 2.25.0
- */
- getStyles: function (entity) {
- if(!entity) return null
- if (this.featureIsSelected(entity)) {
- return this.getSelectedStyles(entity)
- }
- const color = this.colorForEntity(entity);
- if (!color) { return null }
- const outlineColor = this.colorToCesiumColor(
- this.get("outlineColor")?.get("color")
- );
- return {
- "color": color,
- "outlineColor": outlineColor,
- "outline": outlineColor ? true : false,
- "lineWidth": 3,
- "markerSize": 25,
- "pointSize": 13,
- }
- },
-
- /**
- * 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
+ this.resumeEvents();
+ model.runVisualizers();
+ } catch (e) {
+ console.log("Failed to update CesiumVectorData model styles.", e);
+ }
+ },
+
+ /**
+ * 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.getEntities(); // .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
);
- })
- },
-
- });
-
- return CesiumVectorData;
-
- }
-);
+ 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 (e) {
+ console.log("Error getting bounding sphere.", e);
+ });
+ },
+ }
+ );
+
+ return CesiumVectorData;
+});
diff --git a/src/js/models/maps/assets/MapAsset.js b/src/js/models/maps/assets/MapAsset.js
index cccbcf4b0..e02f5391c 100644
--- a/src/js/models/maps/assets/MapAsset.js
+++ b/src/js/models/maps/assets/MapAsset.js
@@ -1,895 +1,930 @@
-'use strict';
-
-define(
- [
- 'jquery',
- 'underscore',
- 'backbone',
- 'models/portals/PortalImage',
- 'models/maps/AssetColorPalette',
- MetacatUI.root + '/components/dayjs.min.js'
- ],
- function (
- $,
- _,
- Backbone,
- PortalImage,
- AssetColorPalette,
- dayjs
- ) {
- /**
- * @classdesc A MapAsset Model comprises information required to fetch source data for
- * some asset or resource that is displayed in a map, such as imagery (raster) tiles,
- * vector data, a 3D tileset, or a terrain model. This model also contains metadata
- * about the source data, like an attribution and a description. It represents the
- * most generic type of asset, and can be extended to support specific assets that are
- * compatible with a given map widget.
- * @classcategory Models/Maps/Assets
- * @class MapAsset
- * @name MapAsset
- * @extends Backbone.Model
- * @since 2.18.0
- * @constructor
- */
- var MapAsset = Backbone.Model.extend(
- /** @lends MapAsset.prototype */ {
-
- /**
- * The name of this type of model
- * @type {string}
- */
- type: 'MapAsset',
-
- /**
- * Default attributes for MapAsset models
- * @name MapAsset#defaults
- * @type {Object}
- * @property {('Cesium3DTileset'|'BingMapsImageryProvider'|'IonImageryProvider'|'WebMapTileServiceImageryProvider'|'TileMapServiceImageryProvider'|'CesiumTerrainProvider')} type
- * The format of the data. Must be one of the supported types.
- * @property {string} label A user friendly name for this asset, to be displayed
- * in a map.
- * @property {string} [icon = '']
- * A PID for an SVG saved as a dataObject, or an SVG string. The SVG will be used
- * as an icon that will be displayed next to the label in the layers list. It
- * should be an SVG file that has no fills, borders, or styles set on it (since
- * the icon will be shaded dynamically by the maps CSS using a fill attribute). It
- * must use a viewbox property rather than a height and width.
- * @property {string} [description = ''] A brief description about the asset, e.g.
- * which area it covers, the resolution, etc.
- * @property {string} [attribution = ''] A credit or attribution to display along
- * with this map resource.
- * @property {string} [moreInfoLink = ''] A link to show in a map where a user can
- * find more information about this resource.
- * @property {string} [downloadLink = ''] A link to show in a map where a user can
- * go to download the source data.
- * @property {string} [id = ''] If this asset's data is archived in a DataONE
- * repository, the ID of the data package.
- * @property {Boolean} [selected = false] Set to true when this asset has been
- * selected by the user in the layer list.
- * @property {Number} [opacity = 1] A number between 0 and 1 indicating the
- * opacity of the layer on the map, with 0 representing fully transparent and 1
- * representing fully opaque. This applies to raster (imagery) and vector assets,
- * not to terrain assets.
- * @property {Boolean} [visible = true] Set to true if the layer is visible on the
- * map, false if it is hidden. This applies to raster (imagery) and vector assets,
- * not to terrain assets.
- * @property {AssetColorPalette} [colorPalette] The color or colors mapped to
- * attributes of this asset. This applies to raster/imagery and vector assets. For
- * imagery, the colorPalette will be used to create a legend. For vector assets
- * (e.g. 3Dtilesets), it will also be used to style the features.
- * @property {MapConfig#FeatureTemplate} [featureTemplate] Configuration for
- * content and layout of the Feature Info panel - the panel that shows information
- * about a selected feature from a vector asset ({@link FeatureInfoView}).
- * @property {Cesium.Entity|Cesium.3DTilesetFeature} [featureType] For vector
- * and 3d tileset assets, the object type that cesium uses to represent features
- * from the asset. Null for imagery and terrain assets.
- * @property {MapConfig#CustomProperties} [customProperties] Configuration that
- * allows for the definition of custom feature properties, potentially based on
- * other properties. For example, a custom property could be a formatted version
- * of an existing date property.
- * @property {MapConfig#Notification} [notification] A custom badge and message to
- * display about the layer in the Layer list. For example, this could highlight
- * the layer if it is new, give a warning if they layer is under development, etc.
- * @property {'ready'|'error'|null} [status = null] Set to 'ready' when the
- * resource is loaded and ready to be rendered in a map view. Set to 'error' when
- * the asset is not supported, or there was a problem requesting the resource.
- * @property {string} [statusDetails = null] Any further details about the status,
- * especially when there was an error.
- */
- defaults: function () {
- return {
- type: '',
- label: '',
- icon: '',
- description: '',
- attribution: '',
- moreInfoLink: '',
- downloadLink: '',
- id: '',
- selected: false,
- opacity: 1,
- visible: true,
- colorPalette: null,
- customProperties: {},
- featureTemplate: {},
- featureType: null,
- notification: {},
- status: null,
- statusDetails: null
- }
- },
-
- /**
- * The source of a specific asset (i.e. layer or terrain data) to show on the map,
- * as well as metadata and display properties of the asset. Some properties listed
- * here do not apply to all asset types, but this is specified in the property
- * description.
- * @typedef {Object} MapAssetConfig
- * @name MapConfig#MapAssetConfig
- * @property {('Cesium3DTileset'|'BingMapsImageryProvider'|'IonImageryProvider'|'WebMapTileServiceImageryProvider'|'WebMapServiceImageryProvider'|'TileMapServiceImageryProvider'|'NaturalEarthII'|'CesiumTerrainProvider'|'GeoJsonDataSource'|'USGSImageryTopo'|'OpenStreetMapImageryProvider')} type -
- * A string indicating the format of the data. Some of these types correspond
- * directly to Cesium classes. The NaturalEarthII type is a special imagery layer
- * that automatically sets the cesiumOptions to load the Natural Earth II imagery
- * that is shipped with Cesium/MetacatUI. If this type is set, then no other
- * cesiumOptions are required. The same is true for USGSImageryTopo, which pulls
- * imagery directly from USGS.
- * @property {(Cesium3DTileset#cesiumOptions|CesiumImagery#cesiumOptions|CesiumTerrain#cesiumOptions|CesiumVectorData#cesiumOptions)} [cesiumOptions] -
- * For MapAssets that are configured for Cesium, like
- * Cesium3DTilesets, an object with options to pass to the Cesium constructor
- * function that creates the Cesium model. Options are specific to each type of
- * asset. For details, see documentation for each of the types.
- * @property {string} label - A user friendly name for this asset, to be displayed
- * in a map.
- * @property {string} [icon] - A PID for an SVG saved as a dataObject, or an SVG
- * string. The SVG will be used as an icon that will be displayed next to the
- * label in the layers list. It should be an SVG file that has no fills, borders,
- * or styles set on it (since the icon will be shaded dynamically by the maps CSS
- * using a fill attribute). It must use a viewbox property rather than a height
- * and width.
- * @property {Number} [opacity=1] - A number between 0 and 1 indicating the
- * opacity of the layer on the map, with 0 representing fully transparent and 1
- * representing fully opaque. This applies to raster (imagery) and vector assets,
- * not to terrain assets.
- * @property {Boolean} [visible=true] - Set to true if the layer is visible on the
- * map, false if it is hidden. This applies to raster (imagery) and vector assets,
- * not to terrain assets.
- * @property {string} [description] - A brief description about the asset, e.g.
- * which area it covers, the resolution, etc.
- * @property {string} [attribution] A credit or attribution to display along with
- * this asset.
- * @property {string} [moreInfoLink] A complete URL used to create a link to show
- * in a map where a user can find more information about this resource.
- * @property {string} [downloadLink] A complete URL used to show a link in a map
- * where a user can go to download the source data.
- * @property {string} [id] If this asset's data is archived in a DataONE
- * repository, the ID of the data package.
- * @property {MapConfig#ColorPaletteConfig} [colorPalette] The color or colors
- * mapped to attributes of this asset. This applies to raster/imagery and vector
- * assets. For imagery, the colorPalette will be used to create a legend. For
- * vector assets (e.g. 3Dtilesets), it will also be used to style the features.
- * @property {MapConfig#FeatureTemplate} [featureTemplate] Configuration for the
- * content and layout of the Feature Info panel ({@link FeatureInfoView}) - the
- * panel that shows information about a selected feature from a vector asset. If
- * no feature template is set, then the default table layout is used.
- * @property {MapConfig#CustomProperties} [customProperties] Definitions of custom
- * properties of features, potentially based on existing properties. For example,
- * a custom property could be a formatted version of another date property. These
- * custom properties can be used in the filters, colorPalette, or featureTemplate.
- * So far, custom strings and formatted dates are supported. Eventually, the
- * custom properties may be expanded to support formatted numbers and booleans.
- * @property {MapConfig#VectorFilterConfig} [filters] - A set of conditions used
- * to show or hide specific features of this tileset.
- * @property {MapConfig#Notification} [notification] A custom badge and message to
- * display about the layer in the Layer list. For example, this could highlight
- * the layer if it is new, give a warning if they layer is under development, etc.
- */
-
- /**
- * A feature template configures the format and content of information displayed
- * in the Feature Info panel ({@link FeatureInfoView}). The Feature Info panel is
- * displayed in a map when a user clicks on a vector feature in a map.
- * @typedef {Object} FeatureTemplate
- * @name MapConfig#FeatureTemplate
- * @since 2.19.0
- * @property {'story'|'table'} [template='table'] The name/ID of the template to
- * use. This must match the name of one of the templates available in
- * {@link FeatureInfoView#contentTemplates}.
- * @property {string} [label] Sets which of the feature properties to use as the
- * title for the FeatureInfoView. The string must exactly match the key for a
- * property that exists in the feature.
- * @property {MapConfig#StoryTemplateOptions} [options] A list of key-value pairs
- * that map the template variable to a property/attribute of the the feature. Keys
- * are the template variable names and values are the names of properties in the
- * feature. Template variable names are specific to each template. Currently only
- * the 'story' template allows variables. These are specified in the
- * {@link FeatureInfoView#contentTemplates}.
- * @example
- * // Use the "story" template, which shows a secondary title, image, description,
- * // and link.
- * {
- * "template": "story",
- * "label": "title",
- * "options": {
- * "subtitle": "formattedDate",
- * "description": "summary",
- * "thumbnail": "imageSrc",
- * "url": "newsLink",
- * "urlText": "newsTitle",
- * }
- * }
- * @example
- * // Use the default template (a table), but use the "forestName" attribute for
- * // the FeatureInfo panel label
- * {
- * "label": "forestName"
- * }
- */
-
- /**
- * An object that maps template variable to feature properties for the "story"
- * template.
- * @typedef {Object}
- * @name MapConfig#StoryTemplateOptions
- * @since 2.19.0
- * @property {string} subtitle The name of a feature property to use for a
- * secondary title in the template
- * @property {string} description The name of a feature property that contains a
- * brief summary or description of the feature; displayed as a paragraph.
- * @property {string} thumbnail The name of a feature property that contains a URL
- * for an image. Displayed as a thumbnail next to the description.
- * @property {string} url The name of a feature property with a URL to use to
- * create a link (e.g. to learn more information about the given feature)
- * @property {string} urlText The name of a feature property that has text to
- * display for the url. Defaults to 'Read More' if none is set.
- */
-
- /**
- * An object where the keys indicate the name/ID of the new custom property to
- * create, and the values are an object that defines the new property.
- * @typedef {Object.} CustomProperties
- * @name MapConfig#CustomProperties
- * @since 2.19.0
- * @example
- * {
- * "year": {
- * "type": "date",
- * "property": "dateTime",
- * "format": "YYYY",
- * },
- * "urlText": {
- * "type": "string",
- * "value": "Click here to learn more about this feature"
- * }
- * }
- */
-
- /**
- * An object that defines a formatted date to use as a property in a feature. Used
- * in the {@link MapConfig#CustomProperties} object.
- * @typedef {Object} CustomDateProperty
- * @name MapConfig#CustomDateProperty
- * @since 2.19.0
- * @property {'date'} type Must be set to 'date' to indicate that this is a custom
- * date property
- * @property {string} property The name/ID of the existing date property to format
- * @property {string} format A string that indicates the new format to use.
- * Follows the syntax used by Day.JS, see
- * {@link https://day.js.org/docs/en/display/format}
- */
-
- /**
- * An object that defines a custom string to use as a property in a feature. Used
- * in the {@link MapConfig#CustomProperties} object.
- * @typedef {Object} CustomStringProperty
- * @name MapConfig#CustomStringProperty
- * @since 2.19.0
- * @property {'string'} type Must be set to 'string' to indicate that this is a
- * custom string property
- * @property {string} value The new string to use. So far only static strings are
- * available. In the future, templates that include other properties may be
- * supported.
- */
-
- /**
- * A notification displays a badge in the {@link LayerListView} and a message in
- * the {@link LayerDetailsView}. This is useful for indicating some special status
- * of the layer: "new", "under development", etc.
- * @typedef {Object} Notification
- * @name MapConfig#Notification
- * @since 2.22.0
- * @property {'yellow'|'green'|'blue'|'contrast'} [style] - The badge and message
- * color. If none is set, then notification elements will be similar to the
- * background colour (subtle).
- * @property {string} badge - The text to display in the badge element next to the
- * layer label in the list. This badge should be as few characters as possible.
- * @property {string} message - A longer message to display explaining the status.
-
- /**
- * Executed when a new MapAsset model is created.
- * @param {MapConfig#MapAssetConfig} [assetConfig] The initial values of the
- * attributes, which will be set on the model.
- */
- initialize: function (assetConfig) {
- try {
-
- const model = this;
-
- if (!assetConfig || typeof assetConfig !== 'object') {
- assetConfig = {}
- } else {
- assetConfig = JSON.parse(JSON.stringify(assetConfig))
- }
+"use strict";
+
+define([
+ "underscore",
+ "backbone",
+ "models/portals/PortalImage",
+ "models/maps/AssetColorPalette",
+ MetacatUI.root + "/components/dayjs.min.js",
+], function (_, Backbone, PortalImage, AssetColorPalette, dayjs) {
+ /**
+ * @classdesc A MapAsset Model comprises information required to fetch source data for
+ * some asset or resource that is displayed in a map, such as imagery (raster) tiles,
+ * vector data, a 3D tileset, or a terrain model. This model also contains metadata
+ * about the source data, like an attribution and a description. It represents the
+ * most generic type of asset, and can be extended to support specific assets that are
+ * compatible with a given map widget.
+ * @classcategory Models/Maps/Assets
+ * @class MapAsset
+ * @name MapAsset
+ * @extends Backbone.Model
+ * @since 2.18.0
+ * @constructor
+ */
+ var MapAsset = Backbone.Model.extend(
+ /** @lends MapAsset.prototype */ {
+ /**
+ * The name of this type of model
+ * @type {string}
+ */
+ type: "MapAsset",
+
+ /**
+ * Default attributes for MapAsset models
+ * @name MapAsset#defaults
+ * @type {Object}
+ * @property {('Cesium3DTileset'|'BingMapsImageryProvider'|'IonImageryProvider'|'WebMapTileServiceImageryProvider'|'TileMapServiceImageryProvider'|'CesiumTerrainProvider')} type
+ * The format of the data. Must be one of the supported types.
+ * @property {string} label A user friendly name for this asset, to be displayed
+ * in a map.
+ * @property {string} [icon = '']
+ * A PID for an SVG saved as a dataObject, or an SVG string. The SVG will be used
+ * as an icon that will be displayed next to the label in the layers list. It
+ * should be an SVG file that has no fills, borders, or styles set on it (since
+ * the icon will be shaded dynamically by the maps CSS using a fill attribute). It
+ * must use a viewbox property rather than a height and width.
+ * @property {string} [description = ''] A brief description about the asset, e.g.
+ * which area it covers, the resolution, etc.
+ * @property {string} [attribution = ''] A credit or attribution to display along
+ * with this map resource.
+ * @property {string} [moreInfoLink = ''] A link to show in a map where a user can
+ * find more information about this resource.
+ * @property {string} [downloadLink = ''] A link to show in a map where a user can
+ * go to download the source data.
+ * @property {string} [id = ''] If this asset's data is archived in a DataONE
+ * repository, the ID of the data package.
+ * @property {Boolean} [selected = false] Set to true when this asset has been
+ * selected by the user in the layer list.
+ * @property {Number} [opacity = 1] A number between 0 and 1 indicating the
+ * opacity of the layer on the map, with 0 representing fully transparent and 1
+ * representing fully opaque. This applies to raster (imagery) and vector assets,
+ * not to terrain assets.
+ * @property {Boolean} [visible = true] Set to true if the layer is visible on the
+ * map, false if it is hidden. This applies to raster (imagery) and vector assets,
+ * not to terrain assets.
+ * @property {AssetColorPalette} [colorPalette] The color or colors mapped to
+ * attributes of this asset. This applies to raster/imagery and vector assets. For
+ * imagery, the colorPalette will be used to create a legend. For vector assets
+ * (e.g. 3Dtilesets), it will also be used to style the features.
+ * @property {MapConfig#FeatureTemplate} [featureTemplate] Configuration for
+ * content and layout of the Feature Info panel - the panel that shows information
+ * about a selected feature from a vector asset ({@link FeatureInfoView}).
+ * @property {Cesium.Entity|Cesium.3DTilesetFeature} [featureType] For vector
+ * and 3d tileset assets, the object type that cesium uses to represent features
+ * from the asset. Null for imagery and terrain assets.
+ * @property {MapConfig#CustomProperties} [customProperties] Configuration that
+ * allows for the definition of custom feature properties, potentially based on
+ * other properties. For example, a custom property could be a formatted version
+ * of an existing date property.
+ * @property {MapConfig#Notification} [notification] A custom badge and message to
+ * display about the layer in the Layer list. For example, this could highlight
+ * the layer if it is new, give a warning if they layer is under development, etc.
+ * @property {'ready'|'error'|null} [status = null] Set to 'ready' when the
+ * resource is loaded and ready to be rendered in a map view. Set to 'error' when
+ * the asset is not supported, or there was a problem requesting the resource.
+ * @property {string} [statusDetails = null] Any further details about the status,
+ * especially when there was an error.
+ * @property {Boolean} [hideInLayerList = false] Set to true to hide this asset
+ * from the layer list.
+ */
+ defaults: function () {
+ return {
+ type: "",
+ label: "",
+ icon: '',
+ description: "",
+ attribution: "",
+ moreInfoLink: "",
+ downloadLink: "",
+ id: "",
+ selected: false,
+ opacity: 1,
+ visible: true,
+ colorPalette: null,
+ customProperties: {},
+ featureTemplate: {},
+ featureType: null,
+ notification: {},
+ status: null,
+ statusDetails: null,
+ hideInLayerList: false,
+ };
+ },
+
+ /**
+ * The source of a specific asset (i.e. layer or terrain data) to show on the map,
+ * as well as metadata and display properties of the asset. Some properties listed
+ * here do not apply to all asset types, but this is specified in the property
+ * description.
+ * @typedef {Object} MapAssetConfig
+ * @name MapConfig#MapAssetConfig
+ * @property {('Cesium3DTileset'|'BingMapsImageryProvider'|'IonImageryProvider'|'WebMapTileServiceImageryProvider'|'WebMapServiceImageryProvider'|'TileMapServiceImageryProvider'|'NaturalEarthII'|'CesiumTerrainProvider'|'GeoJsonDataSource'|'USGSImageryTopo'|'OpenStreetMapImageryProvider')} type -
+ * A string indicating the format of the data. Some of these types correspond
+ * directly to Cesium classes. The NaturalEarthII type is a special imagery layer
+ * that automatically sets the cesiumOptions to load the Natural Earth II imagery
+ * that is shipped with Cesium/MetacatUI. If this type is set, then no other
+ * cesiumOptions are required. The same is true for USGSImageryTopo, which pulls
+ * imagery directly from USGS.
+ * @property {(Cesium3DTileset#cesiumOptions|CesiumImagery#cesiumOptions|CesiumTerrain#cesiumOptions|CesiumVectorData#cesiumOptions)} [cesiumOptions] -
+ * For MapAssets that are configured for Cesium, like
+ * Cesium3DTilesets, an object with options to pass to the Cesium constructor
+ * function that creates the Cesium model. Options are specific to each type of
+ * asset. For details, see documentation for each of the types.
+ * @property {string} label - A user friendly name for this asset, to be displayed
+ * in a map.
+ * @property {string} [icon] - A PID for an SVG saved as a dataObject, or an SVG
+ * string. The SVG will be used as an icon that will be displayed next to the
+ * label in the layers list. It should be an SVG file that has no fills, borders,
+ * or styles set on it (since the icon will be shaded dynamically by the maps CSS
+ * using a fill attribute). It must use a viewbox property rather than a height
+ * and width.
+ * @property {Number} [opacity=1] - A number between 0 and 1 indicating the
+ * opacity of the layer on the map, with 0 representing fully transparent and 1
+ * representing fully opaque. This applies to raster (imagery) and vector assets,
+ * not to terrain assets.
+ * @property {Boolean} [visible=true] - Set to true if the layer is visible on the
+ * map, false if it is hidden. This applies to raster (imagery) and vector assets,
+ * not to terrain assets.
+ * @property {string} [description] - A brief description about the asset, e.g.
+ * which area it covers, the resolution, etc.
+ * @property {string} [attribution] A credit or attribution to display along with
+ * this asset.
+ * @property {string} [moreInfoLink] A complete URL used to create a link to show
+ * in a map where a user can find more information about this resource.
+ * @property {string} [downloadLink] A complete URL used to show a link in a map
+ * where a user can go to download the source data.
+ * @property {string} [id] If this asset's data is archived in a DataONE
+ * repository, the ID of the data package.
+ * @property {MapConfig#ColorPaletteConfig} [colorPalette] The color or colors
+ * mapped to attributes of this asset. This applies to raster/imagery and vector
+ * assets. For imagery, the colorPalette will be used to create a legend. For
+ * vector assets (e.g. 3Dtilesets), it will also be used to style the features.
+ * @property {MapConfig#FeatureTemplate} [featureTemplate] Configuration for the
+ * content and layout of the Feature Info panel ({@link FeatureInfoView}) - the
+ * panel that shows information about a selected feature from a vector asset. If
+ * no feature template is set, then the default table layout is used.
+ * @property {MapConfig#CustomProperties} [customProperties] Definitions of custom
+ * properties of features, potentially based on existing properties. For example,
+ * a custom property could be a formatted version of another date property. These
+ * custom properties can be used in the filters, colorPalette, or featureTemplate.
+ * So far, custom strings and formatted dates are supported. Eventually, the
+ * custom properties may be expanded to support formatted numbers and booleans.
+ * @property {MapConfig#VectorFilterConfig} [filters] - A set of conditions used
+ * to show or hide specific features of this tileset.
+ * @property {MapConfig#Notification} [notification] A custom badge and message to
+ * display about the layer in the Layer list. For example, this could highlight
+ * the layer if it is new, give a warning if they layer is under development, etc.
+ * @property {boolean} [hideInLayerList] - Set to true to hide this asset from the
+ * layer list.
+ */
+
+ /**
+ * A feature template configures the format and content of information displayed
+ * in the Feature Info panel ({@link FeatureInfoView}). The Feature Info panel is
+ * displayed in a map when a user clicks on a vector feature in a map.
+ * @typedef {Object} FeatureTemplate
+ * @name MapConfig#FeatureTemplate
+ * @since 2.19.0
+ * @property {'story'|'table'} [template='table'] The name/ID of the template to
+ * use. This must match the name of one of the templates available in
+ * {@link FeatureInfoView#contentTemplates}.
+ * @property {string} [label] Sets which of the feature properties to use as the
+ * title for the FeatureInfoView. The string must exactly match the key for a
+ * property that exists in the feature.
+ * @property {MapConfig#StoryTemplateOptions} [options] A list of key-value pairs
+ * that map the template variable to a property/attribute of the the feature. Keys
+ * are the template variable names and values are the names of properties in the
+ * feature. Template variable names are specific to each template. Currently only
+ * the 'story' template allows variables. These are specified in the
+ * {@link FeatureInfoView#contentTemplates}.
+ * @example
+ * // Use the "story" template, which shows a secondary title, image, description,
+ * // and link.
+ * {
+ * "template": "story",
+ * "label": "title",
+ * "options": {
+ * "subtitle": "formattedDate",
+ * "description": "summary",
+ * "thumbnail": "imageSrc",
+ * "url": "newsLink",
+ * "urlText": "newsTitle",
+ * }
+ * }
+ * @example
+ * // Use the default template (a table), but use the "forestName" attribute for
+ * // the FeatureInfo panel label
+ * {
+ * "label": "forestName"
+ * }
+ */
+
+ /**
+ * An object that maps template variable to feature properties for the "story"
+ * template.
+ * @typedef {Object}
+ * @name MapConfig#StoryTemplateOptions
+ * @since 2.19.0
+ * @property {string} subtitle The name of a feature property to use for a
+ * secondary title in the template
+ * @property {string} description The name of a feature property that contains a
+ * brief summary or description of the feature; displayed as a paragraph.
+ * @property {string} thumbnail The name of a feature property that contains a URL
+ * for an image. Displayed as a thumbnail next to the description.
+ * @property {string} url The name of a feature property with a URL to use to
+ * create a link (e.g. to learn more information about the given feature)
+ * @property {string} urlText The name of a feature property that has text to
+ * display for the url. Defaults to 'Read More' if none is set.
+ */
+
+ /**
+ * An object where the keys indicate the name/ID of the new custom property to
+ * create, and the values are an object that defines the new property.
+ * @typedef {Object.} CustomProperties
+ * @name MapConfig#CustomProperties
+ * @since 2.19.0
+ * @example
+ * {
+ * "year": {
+ * "type": "date",
+ * "property": "dateTime",
+ * "format": "YYYY",
+ * },
+ * "urlText": {
+ * "type": "string",
+ * "value": "Click here to learn more about this feature"
+ * }
+ * }
+ */
+
+ /**
+ * An object that defines a formatted date to use as a property in a feature. Used
+ * in the {@link MapConfig#CustomProperties} object.
+ * @typedef {Object} CustomDateProperty
+ * @name MapConfig#CustomDateProperty
+ * @since 2.19.0
+ * @property {'date'} type Must be set to 'date' to indicate that this is a custom
+ * date property
+ * @property {string} property The name/ID of the existing date property to format
+ * @property {string} format A string that indicates the new format to use.
+ * Follows the syntax used by Day.JS, see
+ * {@link https://day.js.org/docs/en/display/format}
+ */
+
+ /**
+ * An object that defines a custom string to use as a property in a feature. Used
+ * in the {@link MapConfig#CustomProperties} object.
+ * @typedef {Object} CustomStringProperty
+ * @name MapConfig#CustomStringProperty
+ * @since 2.19.0
+ * @property {'string'} type Must be set to 'string' to indicate that this is a
+ * custom string property
+ * @property {string} value The new string to use. So far only static strings are
+ * available. In the future, templates that include other properties may be
+ * supported.
+ */
+
+ /**
+ * A notification displays a badge in the {@link LayerListView} and a message in
+ * the {@link LayerDetailsView}. This is useful for indicating some special status
+ * of the layer: "new", "under development", etc.
+ * @typedef {Object} Notification
+ * @name MapConfig#Notification
+ * @since 2.22.0
+ * @property {'yellow'|'green'|'blue'|'contrast'} [style] - The badge and message
+ * color. If none is set, then notification elements will be similar to the
+ * background colour (subtle).
+ * @property {string} badge - The text to display in the badge element next to the
+ * layer label in the list. This badge should be as few characters as possible.
+ * @property {string} message - A longer message to display explaining the status.
+ */
+
+ /**
+ * Executed when a new MapAsset model is created.
+ * @param {MapConfig#MapAssetConfig} [assetConfig] The initial values of the
+ * attributes, which will be set on the model.
+ */
+ initialize: function (assetConfig) {
+ try {
+ const model = this;
- // Set the color palette
- if (assetConfig.colorPalette) {
- this.set('colorPalette', new AssetColorPalette(assetConfig.colorPalette))
- }
+ if (!assetConfig || typeof assetConfig !== "object") {
+ assetConfig = {};
+ } else {
+ assetConfig = JSON.parse(JSON.stringify(assetConfig));
+ }
- // The map asset cannot be visible on the map if there was an error loading
- // the asset
- this.listenTo(this, 'change:status', function (model, status) {
- if (status === 'error') {
- this.set('visible', false)
- }
- })
- this.listenTo(this, 'change:visible', function (model, visible) {
- if (this.get('status') === 'error') {
- this.set('visible', false)
- }
- })
+ // Set the color palette
+ if (assetConfig.colorPalette) {
+ this.set(
+ "colorPalette",
+ new AssetColorPalette(assetConfig.colorPalette)
+ );
+ }
- // Fetch the icon, if there is one
- if (assetConfig.icon) {
- if (model.isSVG(assetConfig.icon)) {
- model.updateIcon(assetConfig.icon)
- } else {
- // If the string is not an SVG then assume it is a PID and try to fetch
- // the SVG file. fetchIcon will update the icon when complete.
- model.fetchIcon(assetConfig.icon)
- }
+ // Fetch the icon, if there is one
+ if (assetConfig.icon) {
+ if (model.isSVG(assetConfig.icon)) {
+ model.updateIcon(assetConfig.icon);
+ } else {
+ // If the string is not an SVG then assume it is a PID and try to fetch
+ // the SVG file. fetchIcon will update the icon when complete.
+ model.fetchIcon(assetConfig.icon);
}
+ }
- // Update the style of the asset to highlight the selected features when
- // features from this asset are selected in the map.
- if (typeof this.updateAppearance === 'function') {
+ this.setListeners();
+ } catch (e) {
+ console.log("Error initializing a MapAsset model", e);
+ }
+ },
+
+ /**
+ * Set an error status and message for this asset.
+ * @param {Object|String} error - An error object with a status code
+ * attribute or or string with details about the error.
+ * @since x.x.x
+ */
+ setError: 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;
+ }
+ this.set('status', 'error');
+ this.set('statusDetails', details)
+ },
+
+ /**
+ * Set a ready status for this asset.
+ * @since x.x.x
+ */
+ setReady: function () {
+ this.set('status', 'ready')
+ this.set('statusDetails', null)
+ },
+
+ /**
+ * When the asset can't be loaded, hide it from the map and show an error.
+ * @since x.x.x
+ */
+ handleError: function () {
+ this.set("originalVisibility", this.get("visible"));
+ this.set("visible", false);
+ this.stopListening(this, "change:visible");
+ },
+
+ /**
+ * Set all of the listeners for this model
+ * @since x.x.x
+ */
+ setListeners: function () {
+ try {
+ const status = this.get("status");
+ if (status === "error") {
+ this.handleError();
+ return;
+ } else {
+ const vis = this.get("originalVisibility")
+ if(typeof vis === "boolean"){
+ this.set("visible", vis);
+ }
+ }
+ // The map asset cannot be visible on the map if there was an error
+ // loading the asset
+ this.stopListening(this, "change:status");
+ this.listenTo(this, "change:status", this.setListeners);
+
+ // Listen for changes to the cesiumOptions object
+ this.stopListening(this, "change:cesiumOptions");
+ this.listenTo(this, "change:cesiumOptions", function () {
+ this.createCesiumModel(true);
+ });
- const setSelectFeaturesListeners = function () {
- const mapModel = this.get('mapModel')
- if (!mapModel) { return }
- const selectedFeatures = mapModel.get('selectedFeatures')
+ this.listenToSelectedFeatures();
+ } catch (e) {
+ console.log("Error setting MapAsset Listeners.", e);
+ }
+ },
+
+ /**
+ * Update the appearance of features from this asset when they are
+ * selected or deselected in the map widget.
+ * @since x.x.x
+ */
+ listenToSelectedFeatures: function () {
+ if (typeof this.updateAppearance !== "function") {
+ return;
+ }
+
+ const mapModel = this.get("mapModel");
+ if (!mapModel) {
+ this.listenToOnce(
+ this,
+ "change:mapModel",
+ this.listenToSelectedFeatures
+ );
+ return;
+ }
+
+ const interactions = mapModel.get("interactions");
+
+ if (!interactions) {
+ this.listenToOnce(
+ mapModel,
+ "change:interactions",
+ this.listenToSelectedFeatures
+ );
+ return;
+ }
+
+ const selectedFeatures = mapModel.getSelectedFeatures();
+
+ if(selectedFeatures){
+ this.stopListening(selectedFeatures, "update");
+ this.listenTo(selectedFeatures, "update", this.updateAppearance);
+ }
+
+ // Reset the listeners if the selectedFeatures collection or the
+ // interactions model is replaced
+ this.listenToOnce(interactions, "change:selectedFeatures", function () {
+ this.updateAppearance();
+ this.listenToSelectedFeatures();
+ });
+
+ this.listenToOnce(mapModel, "change:interactions", function () {
+ this.updateAppearance();
+ this.listenToSelectedFeatures();
+ });
+ },
+
+ /**
+ * Get the asset config's cesiumOptions, if it has any. This will return
+ * a copy of the cesiumOptions object, so that changes made to the
+ * returned object will not affect the original cesiumOptions object.
+ * @returns {Object} A copy of the cesiumOptions object, or null if there
+ * are no cesiumOptions.
+ * @since 2.26.0
+ */
+ getCesiumOptions: function () {
+ const cesiumOptions = this.get("cesiumOptions");
+ if (!cesiumOptions) {
+ return null;
+ }
+ return JSON.parse(JSON.stringify(cesiumOptions));
+ },
+
+ /**
+ * Given a feature object from a Feature model, checks if it is part of the
+ * selectedFeatures collection. See featureObject property from
+ * {@link Feature#defaults}. For vector and 3d tile models only.
+ * @param {*} feature - An object that a Map widget uses to represent this feature
+ * in the map, e.g. a Cesium.Entity or a Cesium.Cesium3DTileFeature
+ * @returns {boolean} Returns true if the given feature is part of the
+ * selectedFeatures collection in this asset
+ */
+ featureIsSelected: function (feature) {
+ const map = this.get("mapModel");
+ if (!map) {
+ return false;
+ }
+ const selectedFeatures = map.getSelectedFeatures();
+ if (!selectedFeatures) {
+ return false;
+ }
+ const isSelected = selectedFeatures.containsFeature(feature);
+ return isSelected;
+ },
+
+ /**
+ * Checks if a feature from the map (a Cesium object) is the type of
+ * feature that this map asset model contains. For example, if a
+ * Cesium3DTilesetFeature is passed to this function, this function
+ * will return true if it is a Cesium3DTileset model, and false if it
+ * is a CesiumVectorData model.
+ * @param {Cesium.Cesium3DTilesetFeature|Cesium.Entity} feature
+ * @returns {boolean} true if the feature is an instance of the feature
+ * type set on the asset model, false otherwise.
+ * @since 2.25.0
+ */
+ usesFeatureType: function (feature) {
+ const ft = this.get("featureType");
+ if (!feature || !ft) return false;
+ if (!feature instanceof ft) return false;
+ return true;
+ },
+
+ /**
+ * Given a feature object from a Feature model, checks if it is part of the
+ * selectedFeatures collection. See featureObject property from
+ * {@link Feature#defaults}. For vector and 3d tile models only.
+ * @param {*} feature - An object that a Map widget uses to represent this feature
+ * in the map, e.g. a Cesium.Entity or a Cesium.Cesium3DTileFeature
+ * @returns {boolean} Returns true if the given feature is part of the
+ * selectedFeatures collection in this asset
+ * @since 2.25.0
+ */
+ containsFeature: function (feature) {
+ if (!this.usesFeatureType(feature)) return false;
+ if (!this.getCesiumModelFromFeature) return false;
+ const cesiumModel = this.getCesiumModelFromFeature(feature);
+ if (!cesiumModel) return false;
+ if (this.get("cesiumModel") == cesiumModel) return true;
+ return false;
+ },
+
+ /**
+ * Given a feature object from a Feature model, returns the attributes
+ * needed to create a Feature model. For vector and 3d tile models only.
+ * @param {*} feature - An object that a Map widget uses to represent this feature
+ * in the map, e.g. a Cesium.Entity or a Cesium.Cesium3DTileFeature
+ * @returns {Object} An object with properties, mapAsset, featureID, featureObject,
+ * and label properties. Returns null if the feature is not the correct type
+ * for this asset model.
+ */
+ getFeatureAttributes: function (feature) {
+ if (!this.usesFeatureType(feature)) return null;
+ if (!this.getCesiumModelFromFeature) return null;
+ return {
+ properties: this.getPropertiesFromFeature(feature),
+ mapAsset: this,
+ featureID: this.getIDFromFeature(feature),
+ featureObject: feature,
+ label: this.getLabelFromFeature(feature),
+ };
+ },
+
+ /**
+ * Given a set of properties from a Feature from this Map Asset model, add any
+ * custom properties to the properties object and return it.
+ * @since 2.19.0
+ * @param {Object} properties A set of key-value pairs representing the existing
+ * properties of a feature from this asset.
+ * @returns {Object} The properties object with any custom properties added.
+ */
+ addCustomProperties: function (properties) {
+ try {
+ const model = this;
+ const customProperties = model.get("customProperties");
+ const formattedProperties = {};
- this.stopListening(selectedFeatures, 'update');
- this.listenTo(selectedFeatures, 'update', this.updateAppearance)
+ if (!customProperties || !Object.keys(customProperties).length) {
+ return properties;
+ }
- this.stopListening(mapModel, 'change:selectedFeatures')
- this.listenTo(mapModel, 'change:selectedFeatures', function () {
- this.updateAppearance()
- setSelectFeaturesListeners()
- })
+ if (!properties || typeof properties !== "object") {
+ properties = {};
+ }
+ if (customProperties) {
+ _.each(customProperties, function (config, key) {
+ let formattedValue = "";
+ if (config.type === "date") {
+ formattedValue = model.formatDateProperty(config, properties);
+ // TODO: support formatted numbers and booleans...
+ // } else if (config.type === 'number') {
+ // formattedValue = model.formatNumberProperty(config, properties)
+ // } else if (config.type === 'boolean') {
+ // formattedValue = model.formatBooleanProperty(config, properties)
+ } else {
+ formattedValue = model.formatStringProperty(config, properties);
}
- setSelectFeaturesListeners.call(this)
- this.stopListening(this, 'change:mapModel', setSelectFeaturesListeners)
- this.listenTo(this, 'change:mapModel', setSelectFeaturesListeners)
- }
+ formattedProperties[key] = formattedValue;
+ });
}
- catch (error) {
- console.log(
- 'There was an error initializing a MapAsset model' +
- '. Error details: ' + error
- );
- }
- },
-
- /**
- * Get the asset config's cesiumOptions, if it has any. This will return
- * a copy of the cesiumOptions object, so that changes made to the
- * returned object will not affect the original cesiumOptions object.
- * @returns {Object} A copy of the cesiumOptions object, or null if there
- * are no cesiumOptions.
- * @since 2.26.0
- */
- getCesiumOptions: function () {
- const cesiumOptions = this.get('cesiumOptions')
- if (!cesiumOptions) { return null }
- return JSON.parse(JSON.stringify(cesiumOptions))
- },
-
- /**
- * Given a feature object from a Feature model, checks if it is part of the
- * selectedFeatures collection. See featureObject property from
- * {@link Feature#defaults}. For vector and 3d tile models only.
- * @param {*} feature - An object that a Map widget uses to represent this feature
- * in the map, e.g. a Cesium.Entity or a Cesium.Cesium3DTileFeature
- * @returns {boolean} Returns true if the given feature is part of the
- * selectedFeatures collection in this asset
- */
- featureIsSelected: function (feature) {
- const map = this.get('mapModel')
- if (!map) { return false }
- return map.get('selectedFeatures').containsFeature(feature)
- },
-
- /**
- * Checks if a feature from the map (a Cesium object) is the type of
- * feature that this map asset model contains. For example, if a
- * Cesium3DTilesetFeature is passed to this function, this function
- * will return true if it is a Cesium3DTileset model, and false if it
- * is a CesiumVectorData model.
- * @param {Cesium.Cesium3DTilesetFeature|Cesium.Entity} feature
- * @returns {boolean} true if the feature is an instance of the feature
- * type set on the asset model, false otherwise.
- * @since 2.25.0
- */
- usesFeatureType: function(feature) {
- const ft = this.get("featureType");
- if (!feature || !ft) return false
- if (!feature instanceof ft) return false
- return true
- },
-
- /**
- * Given a feature object from a Feature model, checks if it is part of the
- * selectedFeatures collection. See featureObject property from
- * {@link Feature#defaults}. For vector and 3d tile models only.
- * @param {*} feature - An object that a Map widget uses to represent this feature
- * in the map, e.g. a Cesium.Entity or a Cesium.Cesium3DTileFeature
- * @returns {boolean} Returns true if the given feature is part of the
- * selectedFeatures collection in this asset
- * @since 2.25.0
- */
- containsFeature: function (feature) {
- if (!this.usesFeatureType(feature)) return false
- if (!this.getCesiumModelFromFeature) return false
- const cesiumModel = this.getCesiumModelFromFeature(feature)
- if (!cesiumModel) return false
- if (this.get('cesiumModel') == cesiumModel) return true
- return false
- },
-
- /**
- * Given a feature object from a Feature model, returns the attributes
- * needed to create a Feature model. For vector and 3d tile models only.
- * @param {*} feature - An object that a Map widget uses to represent this feature
- * in the map, e.g. a Cesium.Entity or a Cesium.Cesium3DTileFeature
- * @returns {Object} An object with properties, mapAsset, featureID, featureObject,
- * and label properties. Returns null if the feature is not the correct type
- * for this asset model.
- */
- getFeatureAttributes: function (feature) {
- if (!this.usesFeatureType(feature)) return null;
- if (!this.getCesiumModelFromFeature) return null;
- return {
- properties: this.getPropertiesFromFeature(feature),
- mapAsset: this,
- featureID: this.getIDFromFeature(feature),
- featureObject: feature,
- label: this.getLabelFromFeature(feature),
- }
- },
-
- /**
- * Given a set of properties from a Feature from this Map Asset model, add any
- * custom properties to the properties object and return it.
- * @since 2.19.0
- * @param {Object} properties A set of key-value pairs representing the existing
- * properties of a feature from this asset.
- * @returns {Object} The properties object with any custom properties added.
- */
- addCustomProperties: function (properties) {
- try {
-
- const model = this;
- const customProperties = model.get('customProperties');
- const formattedProperties = {};
-
- if (!customProperties || !Object.keys(customProperties).length) {
- return properties
- }
-
- if (!properties || typeof properties !== 'object') {
- properties = {}
- }
-
- if (customProperties) {
- _.each(customProperties, function (config, key) {
- let formattedValue = '';
- if (config.type === 'date') {
- formattedValue = model.formatDateProperty(config, properties)
- // TODO: support formatted numbers and booleans...
- // } else if (config.type === 'number') {
- // formattedValue = model.formatNumberProperty(config, properties)
- // } else if (config.type === 'boolean') {
- // formattedValue = model.formatBooleanProperty(config, properties)
- } else {
- formattedValue = model.formatStringProperty(config, properties)
- }
- formattedProperties[key] = formattedValue;
- });
- }
- // merge the properties with the formatted properties
- return Object.assign(properties, formattedProperties);
- } catch (error) {
- console.log(
- 'There was an error adding custom properties. Returning properties ' +
- 'unchanged. Error details: ' +
+ // merge the properties with the formatted properties
+ return Object.assign(properties, formattedProperties);
+ } catch (error) {
+ console.log(
+ "There was an error adding custom properties. Returning properties " +
+ "unchanged. Error details: " +
error
- );
- return properties
+ );
+ return properties;
+ }
+ },
+
+ /**
+ * Given a definition for a new date property, and the properties that already
+ * exist on a specific feature, returns a new string with the formatted date.
+ * @since 2.19.0
+ * @param {MapConfig#CustomDateProperty} config - An object that defines the new
+ * date property to create
+ * @param {Object} properties key-value pairs representing existing properties in
+ * a Feature
+ * @returns {string} The value for the new date property, formatted as defined by
+ * config, for the given feature
+ */
+ formatDateProperty: function (config, properties) {
+ try {
+ if (!properties) {
+ properties = {};
}
- },
-
- /**
- * Given a definition for a new date property, and the properties that already
- * exist on a specific feature, returns a new string with the formatted date.
- * @since 2.19.0
- * @param {MapConfig#CustomDateProperty} config - An object that defines the new
- * date property to create
- * @param {Object} properties key-value pairs representing existing properties in
- * a Feature
- * @returns {string} The value for the new date property, formatted as defined by
- * config, for the given feature
- */
- formatDateProperty: function (config, properties) {
- try {
- if (!properties) {
- properties = {}
- }
- let formattedDate = ''
- if (!config || !config.format) {
- return formattedDate;
- }
- const value = properties[config.property];
- if (value) {
- formattedDate = dayjs(value).format(config.format);
- }
+ let formattedDate = "";
+ if (!config || !config.format) {
return formattedDate;
}
- catch (error) {
- console.log(
- 'There was an error formatting a date for a Feature model' +
- '. Error details: ' + error
- );
- return '';
+ const value = properties[config.property];
+ if (value) {
+ formattedDate = dayjs(value).format(config.format);
}
- },
-
- /**
- * For a given set of Feature properties and a definition for a new sting
- * property, returns the value of the custom property. Note that since only static
- * strings are supported so far, this function essentially just returns the value
- * of config.value. This function exists to allow support of dynamic strings in
- * the future (e.g. combining strings from existing properties)
- * @since 2.19.0
- * @param {MapConfig#CustomStringProperty} config The object the defines the new
- * custom property
- * @param {Object} properties key-value pairs representing existing properties in
- * a Feature
- * @returns {string} The new string for the given Feature property
- */
- formatStringProperty: function (config, properties) {
- try {
- if (!properties) {
- properties = {}
- }
- let formattedString = ''
- if (!config || !config.value) {
- return formattedString;
- }
- formattedString = config.value;
- return formattedString;
+ return formattedDate;
+ } catch (error) {
+ console.log(
+ "There was an error formatting a date for a Feature model" +
+ ". Error details: " +
+ error
+ );
+ return "";
+ }
+ },
+
+ /**
+ * For a given set of Feature properties and a definition for a new sting
+ * property, returns the value of the custom property. Note that since only static
+ * strings are supported so far, this function essentially just returns the value
+ * of config.value. This function exists to allow support of dynamic strings in
+ * the future (e.g. combining strings from existing properties)
+ * @since 2.19.0
+ * @param {MapConfig#CustomStringProperty} config The object the defines the new
+ * custom property
+ * @param {Object} properties key-value pairs representing existing properties in
+ * a Feature
+ * @returns {string} The new string for the given Feature property
+ */
+ formatStringProperty: function (config, properties) {
+ try {
+ if (!properties) {
+ properties = {};
}
- catch (error) {
- console.log(
- 'There was an error formatting a string for a Feature model' +
- '. Error details: ' + error
- );
- return '';
+ let formattedString = "";
+ if (!config || !config.value) {
+ return formattedString;
}
- },
-
- // formatNumberProperty: function (config, properties) {
- // try {
- // if (!properties) {
- // properties = {}
- // }
- // let formattedNumber = ''
- // // TODO...
- // }
- // catch (error) {
- // console.log(
- // 'There was an error formatting a number for a Feature model' +
- // '. Error details: ' + error
- // );
- // return '';
- // }
- // },
-
- // formatBooleanProperty: function (config, properties) {
- // try {
- // if (!properties) {
- // properties = {}
- // }
- // let formattedBoolean = ''
- // // TODO...
- // }
- // catch (error) {
- // console.log(
- // 'There was an error formatting a boolean for a Feature model' +
- // '. Error details: ' + error
- // );
- // return '';
- // }
- // },
-
- /**
- * Sanitizes an SVG string and updates the model's 'icon' attribute the sanitized
- * string. Also sets the 'iconStatus' attribute to 'success'.
- * @param {string} icon An SVG string to use for the MapAsset icon
- */
- updateIcon: function (icon) {
- const model = this
- try {
- model.sanitizeIcon(icon, function (sanitizedIcon) {
- model.set('icon', sanitizedIcon)
- model.set('iconStatus', 'success')
+ formattedString = config.value;
+ return formattedString;
+ } catch (error) {
+ console.log(
+ "There was an error formatting a string for a Feature model" +
+ ". Error details: " +
+ error
+ );
+ return "";
+ }
+ },
+
+ // formatNumberProperty: function (config, properties) {
+ // try {
+ // if (!properties) {
+ // properties = {}
+ // }
+ // let formattedNumber = ''
+ // // TODO...
+ // }
+ // catch (error) {
+ // console.log(
+ // 'There was an error formatting a number for a Feature model' +
+ // '. Error details: ' + error
+ // );
+ // return '';
+ // }
+ // },
+
+ // formatBooleanProperty: function (config, properties) {
+ // try {
+ // if (!properties) {
+ // properties = {}
+ // }
+ // let formattedBoolean = ''
+ // // TODO...
+ // }
+ // catch (error) {
+ // console.log(
+ // 'There was an error formatting a boolean for a Feature model' +
+ // '. Error details: ' + error
+ // );
+ // return '';
+ // }
+ // },
+
+ /**
+ * Sanitizes an SVG string and updates the model's 'icon' attribute the sanitized
+ * string. Also sets the 'iconStatus' attribute to 'success'.
+ * @param {string} icon An SVG string to use for the MapAsset icon
+ */
+ updateIcon: function (icon) {
+ const model = this;
+ try {
+ model.sanitizeIcon(icon, function (sanitizedIcon) {
+ model.set("icon", sanitizedIcon);
+ model.set("iconStatus", "success");
+ });
+ } catch (error) {
+ console.log(
+ "There was an error updating an icon in a MapAsset model" +
+ ". Error details: " +
+ error
+ );
+ }
+ },
+
+ /**
+ * Simple test to see if a string is an SVG
+ * @param {string} str The string to check
+ * @returns {Boolean} Returns true if the string starts with `