From c89ff998c94dcb66a022e7c17ed79aa869640df2 Mon Sep 17 00:00:00 2001 From: Robyn Thiessen-Bock Date: Thu, 5 Oct 2023 20:01:35 -0400 Subject: [PATCH] Speed up vector layer rendering, fix drawing - Fix issues with drawing polygons, including drawing polygons over poles - Allow drawn polygons to have properties set like other layers (color, opacity, etc) - When drawing, draw both points and polygons - Add CustomDataSource support - Greatly reduce the number of re-renders Cesium must do (improve map performance) - Add connectors between GeoPoints collection and polygons & points Entities Issues #2180 and #2189 --- src/js/collections/maps/GeoPoints.js | 17 +- src/js/collections/maps/MapAssets.js | 2 +- src/js/models/connectors/GeoPoints-Cesium.js | 163 ++ .../connectors/GeoPoints-CesiumPoints.js | 169 ++ .../connectors/GeoPoints-CesiumPolygon.js | 73 + .../models/connectors/GeoPoints-VectorData.js | 171 --- src/js/models/maps/GeoPoint.js | 27 +- src/js/models/maps/GeoUtilities.js | 5 +- src/js/models/maps/Geohash.js | 15 +- src/js/models/maps/MapInteraction.js | 7 +- src/js/models/maps/assets/CesiumGeohash.js | 4 +- src/js/models/maps/assets/CesiumVectorData.js | 1360 +++++++++-------- src/js/models/maps/assets/MapAsset.js | 31 + src/js/views/maps/CesiumWidgetView.js | 96 +- src/js/views/maps/DrawToolView.js | 90 +- 15 files changed, 1270 insertions(+), 960 deletions(-) create mode 100644 src/js/models/connectors/GeoPoints-Cesium.js create mode 100644 src/js/models/connectors/GeoPoints-CesiumPoints.js create mode 100644 src/js/models/connectors/GeoPoints-CesiumPolygon.js delete mode 100644 src/js/models/connectors/GeoPoints-VectorData.js diff --git a/src/js/collections/maps/GeoPoints.js b/src/js/collections/maps/GeoPoints.js index 48ee17555..d225ee0be 100644 --- a/src/js/collections/maps/GeoPoints.js +++ b/src/js/collections/maps/GeoPoints.js @@ -190,10 +190,8 @@ define(["backbone", "models/maps/GeoPoint"], function (Backbone, GeoPoint) { */ getCZMLPolygon: function () { const coords = this.toECEFArray(); - // make a random ID: - const id = "polygon_" + Math.floor(Math.random() * 1000000); return { - id: id, + id: this.cid, name: "Polygon", polygon: { positions: { @@ -309,6 +307,19 @@ define(["backbone", "models/maps/GeoPoint"], function (Backbone, GeoPoint) { 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"); + }); + }, } ); diff --git a/src/js/collections/maps/MapAssets.js b/src/js/collections/maps/MapAssets.js index 1dc3a6d7b..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, }, { diff --git a/src/js/models/connectors/GeoPoints-Cesium.js b/src/js/models/connectors/GeoPoints-Cesium.js new file mode 100644 index 000000000..3fc55b160 --- /dev/null +++ b/src/js/models/connectors/GeoPoints-Cesium.js @@ -0,0 +1,163 @@ +"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 { + // Listen for changes to the points collection and update the layer + let 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 () { + this.stopListening(this.get("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..5d726d745 --- /dev/null +++ b/src/js/models/connectors/GeoPoints-CesiumPolygon.js @@ -0,0 +1,73 @@ +"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 layer = this.get("layer") || this.setVectorLayer(); + const geoPoints = this.get("geoPoints") || this.setPoints(); + return layer.addEntity({ + 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/GeoPoints-VectorData.js b/src/js/models/connectors/GeoPoints-VectorData.js deleted file mode 100644 index 95a7a9b3a..000000000 --- a/src/js/models/connectors/GeoPoints-VectorData.js +++ /dev/null @@ -1,171 +0,0 @@ -/*global define */ -define([ - "backbone", - "collections/maps/GeoPoints", - "models/maps/assets/CesiumVectorData", -], function (Backbone, GeoPoints, CesiumVectorData) { - "use strict"; - - /** - * @class PointsVectorDataConnector - * @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 the features - * created from the points in the collection. - * @name PointsVectorDataConnector - * @extends Backbone.Model - * @constructor - * @classcategory Models/Connectors - * @since x.x.x - * - * TODO: Extend to allow for a collection of GeoPoints collections, where each - * GeoPoints collection can be represented as a different polygon in the - * CesiumVectorData model. - */ - return Backbone.Model.extend( - /** @lends PointsVectorDataConnector.prototype */ { - /** - * The type of Backbone.Model this is. - * @type {string} - * @default "PointsVectorDataConnector" - */ - type: "PointsVectorDataConnector", - - /** - * Extends the default Backbone.Model.defaults() function to specify - * default attributes for the PointsVectorDataConnector model. - */ - defaults: function () { - return { - points: null, - vectorLayer: null, - isConnected: false, - }; - }, - - /** - * Initialize the model. - * @param {Object} attrs - The attributes for this model. - * @param {GeoPoints | Object} [attributes.points] - The GeoPoints - * collection to use for this connector or a JSON object with options to - * create a new GeoPoints collection. If not provided, a new GeoPoints - * collection will be created. - * @param {CesiumVectorData | Object} [attributes.vectorLayer] - 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. - */ - initialize: function (attrs) { - try { - attrs = attrs || {}; - this.setPoints(attrs.points); - this.setVectorLayer(attrs.vectorLayer); - if (attrs.isConnected) { - this.connect(); - } - } catch (e) { - console.log("Error initializing a PointsVectorDataConnector", 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 a JSON object with options to create a new - * GeoPoints collection. If not provided, a new GeoPoints collection will - * be created. - * @returns {GeoPoints} The GeoPoints collection for this connector. - */ - setPoints: function (points) { - if (points instanceof GeoPoints) { - this.set("points", points); - } else { - this.set("points", new GeoPoints(points)); - } - return this.get("points"); - }, - - /** - * Set or create and set the CesiumVectorData model for this connector. - * @param {CesiumVectorData | Object} [vectorLayer] - 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. - */ - setVectorLayer: function (vectorLayer) { - if (vectorLayer instanceof CesiumVectorData) { - this.set("vectorLayer", vectorLayer); - } else { - this.set("vectorLayer", new CesiumVectorData(vectorLayer)); - } - return this.get("vectorLayer"); - }, - - /** - * Listen for changes to the Points collection and update the - * CesiumVectorData model with the features created from the points in - * the collection. - */ - connect: function () { - try { - const connector = this; - this.disconnect(); - - const handler = (this.eventHandler = new Backbone.Model()); - const points = this.get("points") || this.setPoints(); - - // Update the vectorLayer when the points collection is updated. - handler.listenTo(points, "update reset", () => { - connector.updateVectorLayer(); - }); - - // Restart listeners the points collection or the vectorLayer is - // replaced with a new collection or model. - handler.listenToOnce(this, "change:points change:vectorLayer", () => { - if (this.get("isConnected")) { - connector.connect(); - } - }); - - this.set("isConnected", true); - } catch (e) { - console.warn( - "Error connecting a PointsVectorDataConnector, disconnecting.", - e - ); - connector.disconnect(); - } - }, - - /** - * Stop listening for changes to the Points collection. - */ - disconnect: function () { - const handler = this.eventHandler; - if (handler) { - handler.stopListening(); - handler.clear(); - handler = null; - } - this.set("isConnected", false); - }, - - /** - * Update the CesiumVectorData model with the features created from the - * points in the collection. - */ - updateVectorLayer: function () { - const points = this.get("points") || this.setPoints(); - const layer = this.get("vectorLayer") || this.setVectorLayer(); - const type = model.get("type"); - const geom = "Polygon"; - const data = type === "geojson" ? points.toGeoJson(geom) : this.toCzml(geom); - const opts = layer.getCesiumOptions() || {}; - opts.data = data; - layer.set("cesiumOptions", opts); - }, - } - ); -}); diff --git a/src/js/models/maps/GeoPoint.js b/src/js/models/maps/GeoPoint.js index 2ff0d7415..ea73452a1 100644 --- a/src/js/models/maps/GeoPoint.js +++ b/src/js/models/maps/GeoPoint.js @@ -1,6 +1,9 @@ "use strict"; -define(["backbone", "models/maps/GeoUtilities"], function (Backbone, GeoUtilities) { +define(["backbone", "models/maps/GeoUtilities"], function ( + Backbone, + GeoUtilities +) { /** * @class GeoPoint * @classdesc The GeoPoint model stores geographical coordinates including @@ -26,12 +29,16 @@ define(["backbone", "models/maps/GeoUtilities"], function (Backbone, GeoUtilitie * @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 + height: null, + mapWidgetCoords: null, }; }, @@ -51,7 +58,7 @@ define(["backbone", "models/maps/GeoUtilities"], function (Backbone, GeoUtilitie toGeoJsonGeometry: function () { return { type: "Point", - coordinates: this.to2DArray() + coordinates: this.to2DArray(), }; }, @@ -64,7 +71,7 @@ define(["backbone", "models/maps/GeoUtilities"], function (Backbone, GeoUtilitie return { type: "Feature", geometry: this.toGeoJsonGeometry(), - properties: {} + properties: {}, }; }, @@ -83,8 +90,8 @@ define(["backbone", "models/maps/GeoUtilities"], function (Backbone, GeoUtilitie heightReference: "CLAMP_TO_GROUND", }, position: { - cartesian: ecefCoord - } + cartesian: ecefCoord, + }, }; }, @@ -109,11 +116,11 @@ define(["backbone", "models/maps/GeoUtilities"], function (Backbone, GeoUtilitie * Validate the model attributes * @param {Object} attrs - The model's attributes */ - validate: function(attrs) { + 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."; } @@ -121,10 +128,10 @@ define(["backbone", "models/maps/GeoUtilities"], function (Backbone, GeoUtilitie // 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') { + if (typeof attrs.height !== "number") { return "Invalid height. Must be a number."; } - } + }, } ); diff --git a/src/js/models/maps/GeoUtilities.js b/src/js/models/maps/GeoUtilities.js index 4ea00f806..62e51cddc 100644 --- a/src/js/models/maps/GeoUtilities.js +++ b/src/js/models/maps/GeoUtilities.js @@ -1,9 +1,6 @@ "use strict"; -define(["backbone", "models/maps/GeoUtilities"], function ( - Backbone, - GeoUtilities -) { +define(["backbone"], function (Backbone) { /** * @class GeoUtilities * @classdesc The GeoUtilities model has methods foe handling spatial data diff --git a/src/js/models/maps/Geohash.js b/src/js/models/maps/Geohash.js index 24372d065..4f9e865a2 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 @@ -346,7 +347,7 @@ define(["jquery", "underscore", "backbone", "nGeohash"], function ( * @returns {Array} The ECEF coordinates. */ geodeticToECEF: function (coord) { - return GeoUtilities.geodeticToECEF(coord); + return GeoUtilities.prototype.geodeticToECEF(coord); }, } ); diff --git a/src/js/models/maps/MapInteraction.js b/src/js/models/maps/MapInteraction.js index fdc15e83a..bf731a408 100644 --- a/src/js/models/maps/MapInteraction.js +++ b/src/js/models/maps/MapInteraction.js @@ -168,10 +168,11 @@ define([ */ setClickedPositionFromMousePosition: function () { const mousePosition = this.get("mousePosition"); - // get just the longitude and latitude const coords = { longitude: mousePosition.get("longitude"), - latitude: mousePosition.get("latitude") + latitude: mousePosition.get("latitude"), + height: mousePosition.get("height"), + mapWidgetCoords: mousePosition.get("mapWidgetCoords"), }; this.setClickedPosition(coords); }, @@ -191,7 +192,7 @@ define([ if (!point) { point = new GeoPoint(); this.set(attributeName, point); - } + } point.set(position); return point; }, diff --git a/src/js/models/maps/assets/CesiumGeohash.js b/src/js/models/maps/assets/CesiumGeohash.js index b4f23328f..499a716f5 100644 --- a/src/js/models/maps/assets/CesiumGeohash.js +++ b/src/js/models/maps/assets/CesiumGeohash.js @@ -277,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); diff --git a/src/js/models/maps/assets/CesiumVectorData.js b/src/js/models/maps/assets/CesiumVectorData.js index b58b2e62a..e95ec7ab6 100644 --- a/src/js/models/maps/assets/CesiumVectorData.js +++ b/src/js/models/maps/assets/CesiumVectorData.js @@ -1,674 +1,746 @@ -'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 { +"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)); + } - if (!assetConfig) 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 }) + ); + } - MapAsset.prototype.initialize.call(this, assetConfig); + if ( + assetConfig.highlightColor && + !(assetConfig.highlightColor instanceof AssetColor) + ) { + this.set( + "highlightColor", + new AssetColor({ color: assetConfig.highlightColor }) + ); + } - if (assetConfig.filters) { - this.set('filters', new VectorFilters(assetConfig.filters)) + 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(); } + } - // 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 }) - ); - } + model.set("displayReady", false); + model.resetStatus(); - this.createCesiumModel(); + if (typeof dataSourceFunction !== "function") { + model.setError(`${type} is not a supported data type.`); + return; + } + if (!dataSource) { + dataSource = new dataSourceFunction(label); + } + if (!dataSource) { + model.setError("Failed to create a Cesium DataSource model."); + return; + } + // There is no data to load for a CustomDataSource + if (type === "CustomDataSource") { + model.set("cesiumModel", dataSource); + model.setListeners(); + model.setReady(); + model.runVisualizers(); + return; } - catch (error) { - console.log('Error initializing a CesiumVectorData model.', error); + + // For GeoJSON and CZML data sources + if (!cesiumOptions || !cesiumOptions.data) { + model.setError( + "No data was provided 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) { + const data = JSON.parse(JSON.stringify(cesiumOptions.data)); + delete cesiumOptions.data; + + dataSource + .load(data, cesiumOptions) + .then(function (loadedData) { + model.set("cesiumModel", loadedData); 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.setListeners(); } + 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.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 - } - - if (dataSourceFunction && typeof dataSourceFunction === 'function') { - - if (!recreate || !dataSource) { - dataSource = new dataSourceFunction(label) + 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 cesiumModel = this.get("cesiumModel"); + this.set("displayReady", false); + + if (!cesiumModel) { + return; + } - const data = cesiumOptions.data; - delete cesiumOptions.data + const entities = cesiumModel.entities.values; - if (!dataSource) { - model.set('status', 'error') - model.set('statusDetails', 'Failed to create a Cesium DataSource model.') - return - } + // Suspending events while updating a large number of entities helps + // performance. + cesiumModel.entities.suspendEvents(); - 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) - }) - } 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 + if (!model.isVisible()) { + cesiumModel.entities.show = false; + if (model.get("opacity") === 0) model.set("visible", false); + } else { + cesiumModel.entities.show = true; + this.styleEntities(entities); } - catch (error) { - console.log('Failed to create a VectorData Cesium Model.', error); + + cesiumModel.entities.resumeEvents(); + this.runVisualizers(); + } catch (e) { + console.log("Failed to update CesiumVectorData model styles.", e); + } + }, + + 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 = dataSource.update(time); + for (let x = 0; x < visualizers.length; x++) { + displayReadyNow = visualizers[x].update(time) && displayReadyNow; + } + this.set("displayReady", displayReadyNow); + 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 {*} callBack + * @param {*} maxPings + */ + 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 { - 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) + const visualizers = model.get("cesiumModel")._visualizers; + if (visualizers && visualizers.length) { + clearInterval(interval); + callBack(); } - catch (error) { - console.log('Failed to set CesiumVectorData listeners.', error); + }, pingRate); + }, + + getEntityCollection: function () { + const model = this; + const dataSource = model.get("cesiumModel"); + return dataSource?.entities; + }, + + getEntities: function () { + return this.getEntityCollection()?.values || []; + }, + + suspendEvents: function () { + const entities = this.getEntityCollection(); + if (entities) entities.suspendEvents(); + }, + + resumeEvents: function () { + const entities = this.getEntityCollection(); + if (entities) entities.resumeEvents(); + }, + + 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); + } + }, + + removeEntity: function (entity) { + try { + const entities = this.getEntities(); + 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: 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()) + 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'); - - if (!cesiumModel) return - - 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; + }, 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); } - 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) + entity.show = visible; } - 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) + + 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 + ); + if (state === Cesium.BoundingSphereState.PENDING) { + return false; + } else if (state !== Cesium.BoundingSphereState.FAILED) { + boundingSpheres.push( + Cesium.BoundingSphere.clone(boundingSphereScratch) + ); } - 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) { + if (boundingSpheres.length) { + return Cesium.BoundingSphere.fromBoundingSpheres(boundingSpheres); + } + return false; + }) + .catch(function (error) { console.log( - 'There was an error updating CesiumVectorData feature visibility' + - '. Error details: ' + error + "Failed to get the bounding sphere for a CesiumVectorData model" + + ". Error details: " + + error ); - } - }, - - /** - * Waits for the model to be ready to display, then gets a Cesium Bounding Sphere - * that can be used to navigate to view the full extent of the vector data. See - * {@link https://cesium.com/learn/cesiumjs/ref-doc/BoundingSphere.html}. - * @param {Cesium.DataSourceDisplay} dataSourceDisplay The data source display - * attached to the CesiumWidget scene that this bounding sphere is for. Required. - * @returns {Promise} Returns a promise that resolves to a Cesium Bounding Sphere - * when ready - */ - getBoundingSphere: function (dataSourceDisplay) { - return this.whenDisplayReady() - .then(function (model) { - const entities = model.get('cesiumModel').entities.values.slice(0) - const boundingSpheres = []; - const boundingSphereScratch = new Cesium.BoundingSphere(); - for (let i = 0, len = entities.length; i < len; i++) { - let state = Cesium.BoundingSphereState.PENDING; - state = dataSourceDisplay.getBoundingSphere( - entities[i], false, boundingSphereScratch - ) - if (state === Cesium.BoundingSphereState.PENDING) { - return false; - } else if (state !== Cesium.BoundingSphereState.FAILED) { - boundingSpheres.push(Cesium.BoundingSphere.clone(boundingSphereScratch)); - } - } - if (boundingSpheres.length) { - return Cesium.BoundingSphere.fromBoundingSpheres(boundingSpheres); - } - return false - }).catch(function (error) { - console.log( - 'Failed to get the bounding sphere for a CesiumVectorData model' + - '. Error details: ' + error - ); - }) - }, - - }); - - return CesiumVectorData; + }); + }, + } + ); - } -); + return CesiumVectorData; +}); diff --git a/src/js/models/maps/assets/MapAsset.js b/src/js/models/maps/assets/MapAsset.js index f50d34bc9..e84fd5320 100644 --- a/src/js/models/maps/assets/MapAsset.js +++ b/src/js/models/maps/assets/MapAsset.js @@ -333,6 +333,37 @@ define([ } }, + /** + * 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 diff --git a/src/js/views/maps/CesiumWidgetView.js b/src/js/views/maps/CesiumWidgetView.js index 641592e87..51b32a6c9 100644 --- a/src/js/views/maps/CesiumWidgetView.js +++ b/src/js/views/maps/CesiumWidgetView.js @@ -86,7 +86,7 @@ define([ removeFunction: "remove3DTileset", }, { - types: ["GeoJsonDataSource", "CzmlDataSource"], + types: ["GeoJsonDataSource", "CzmlDataSource", "CustomDataSource"], renderFunction: "addVectorData", removeFunction: "removeVectorData", }, @@ -240,8 +240,8 @@ define([ /** * Create a DataSourceDisplay and DataSourceCollection for the Cesium - * widget, and listen to the clock tick to update the display. This is - * required to display vector data (e.g. GeoJSON) on the map. + * widget. This is required to display vector data (e.g. GeoJSON) on the + * map. * @since x.x.x * @returns {Cesium.DataSourceDisplay} The Cesium DataSourceDisplay */ @@ -252,11 +252,6 @@ define([ scene: view.scene, dataSourceCollection: view.dataSourceCollection, }); - view.clock.onTick.removeEventListener( - view.updateDataSourceDisplay, - view - ); - view.clock.onTick.addEventListener(view.updateDataSourceDisplay, view); return view.dataSourceDisplay; }, @@ -285,64 +280,32 @@ define([ if (view.zoomTarget) { view.completeFlight(view.zoomTarget, view.zoomOptions); } + // The dataSourceDisplay must be set to 'ready' to get bounding + // spheres for dataSources + view.dataSourceDisplay._ready = true; } catch (e) { console.log("Error calling post render functions:", e); } }, /** - * Runs on every Cesium clock tick. Updates the display of the - * CesiumVectorData models in the scene. Similar to - * Cesium.DataSourceDisplay.update function, in that it runs update() on - * each DataSource and each DataSource's visualizer, except that it also - * updates each CesiumVectorData model's 'displayReady' attribute. (Sets - * to true when the asset is ready to be rendered in the map, false - * otherwise). Also re-renders the scene when the displayReady attribute - * changes. + * Run the update method and all visualizers for each data source. + * @since x.x.x */ - updateDataSourceDisplay: function () { - try { - const view = this; - const layers = view.model.get("layers"); - - var dataSources = view.dataSourceDisplay.dataSources; - if (!dataSources || !dataSources.length) { - return; - } - - let allReady = true; - const allReadyBefore = view.dataSourceDisplay._ready; - - for (let i = 0, len = dataSources.length; i < len; i++) { - const time = view.clock.currentTime; - const dataSource = dataSources.get(i); - const visualizers = dataSource._visualizers; - - const assetModel = layers.findWhere({ - cesiumModel: dataSource, - }); - let displayReadyNow = dataSource.update(time); - - for (let x = 0; x < visualizers.length; x++) { - displayReadyNow = visualizers[x].update(time) && displayReadyNow; - } - - assetModel.set("displayReady", displayReadyNow); - - allReady = displayReadyNow && allReady; - } - - // If any dataSource has switched display states, then re-render the - // scene. - if (allReady !== allReadyBefore) { - view.scene.requestRender(); - } - // The dataSourceDisplay must be set to 'ready' to get bounding - // spheres for dataSources - view.dataSourceDisplay._ready = allReady; - } catch (e) { - console.log("Error updating the data source display.", e); + updateAllDataSources: function () { + const view = this; + const dataSources = view.dataSourceDisplay.dataSources; + if (!dataSources || !dataSources.length) { + return; } + const time = view.clock.currentTime; + dataSources.forEach(function (dataSource) { + dataSource.update(view.clock.currentTime); + // for each visualizer, update it + dataSource._visualizers.forEach(function (visualizer) { + visualizer.update(time); + }); + }); }, /** @@ -554,10 +517,18 @@ define([ let newPosition = null; if (cartesian) { newPosition = view.getDegreesFromCartesian(cartesian); + newPosition.mapWidgetCoords = cartesian; } view.interactions.setMousePosition(newPosition); }, + /** + * Record the feature hovered over by the mouse based on position. + * @param {Object} position - The position of the mouse on the map + * @param {number} [delay=200] - The minimum number of milliseconds that + * must pass between calls to this function. + * @since x.x.x + */ setHoveredFeatures: function (position, delay = 200) { const view = this; const lastCall = this.setHoveredFeaturesLastCall || 0; @@ -568,14 +539,15 @@ define([ view.interactions.setHoveredFeatures([pickedFeature]); }, + /** + * React when the user interacts with the map. + * @since x.x.x + */ setInteractionListeners: function () { - // TODO: unset listeners too const interactions = this.interactions; const hoveredFeatures = interactions.get("hoveredFeatures"); + this.stopListening(hoveredFeatures, "change update"); this.listenTo(hoveredFeatures, "change update", this.updateCursor); - // this.listenTo( interactions, "change update", - // this.handleClickedFeatures - // ); }, /** diff --git a/src/js/views/maps/DrawToolView.js b/src/js/views/maps/DrawToolView.js index 75fa7dc17..768f4499c 100644 --- a/src/js/views/maps/DrawToolView.js +++ b/src/js/views/maps/DrawToolView.js @@ -1,8 +1,10 @@ "use strict"; -define(["backbone", "models/connectors/GeoPoints-VectorData"], function ( +define(["backbone", "models/connectors/GeoPoints-CesiumPolygon", "models/connectors/GeoPoints-CesiumPoints", "collections/maps/GeoPoints"], function ( Backbone, - GeoPointsVectorData + GeoPointsVectorData, + GeoPointsCesiumPoints, + GeoPoints ) { /** * @class DrawTool @@ -107,6 +109,19 @@ define(["backbone", "models/connectors/GeoPoints-VectorData"], function ( */ points: undefined, + /** + * The color of the polygon that is being drawn as a hex string. + * @type {string} + */ + color: "#a31840", + + /** + * The initial opacity of the polygon that is being drawn. A number + * between 0 and 1. + * @type {number} + */ + opacity: 0.8, + /** * Initializes the DrawTool * @param {Object} options - A literal object with options to pass to the @@ -126,7 +141,7 @@ define(["backbone", "models/connectors/GeoPoints-VectorData"], function ( // points, and originalAction properties to this view this.setUpMapModel(); this.setUpLayer(); - this.setUpConnector(); + this.setUpConnectors(); }, /** @@ -148,21 +163,20 @@ define(["backbone", "models/connectors/GeoPoints-VectorData"], function ( */ setUpLayer: function () { this.layer = this.mapModel.addAsset({ - type: "CzmlDataSource", + type: "CustomDataSource", label: "Your Polygon", description: "The polygon that you are drawing on the map", hideInLayerList: true, // TODO: Hide in LayerList, doc in mapConfig - outlineColor: "#FF3E41", // TODO - opacity: 0.55, // TODO + outlineColor: this.color, + opacity: this.opacity, colorPalette: { colors: [ { - color: "#FF3E41", // TODO + color: this.color, }, ], }, - }); - return this.layer; + }) }, /** @@ -171,12 +185,18 @@ define(["backbone", "models/connectors/GeoPoints-VectorData"], function ( * this view. * @returns {GeoPointsVectorData} The connector */ - setUpConnector: function () { - this.connector = new GeoPointsVectorData({ - vectorLayer: this.layer, + setUpConnectors: function () { + const points = this.points = new GeoPoints(); + this.polygonConnector = new GeoPointsVectorData({ + layer: this.layer, + geoPoints: points, + }); + this.pointsConnector = new GeoPointsCesiumPoints({ + layer: this.layer, + geoPoints: points, }); - this.points = this.connector.get("points"); - this.connector.connect(); + this.polygonConnector.connect(); + this.pointsConnector.connect(); return this.connector; }, @@ -203,6 +223,7 @@ define(["backbone", "models/connectors/GeoPoints-VectorData"], function ( */ removeLayer: function () { if (!this.mapModel || !this.layer) return; + // TODO this.connector.disconnect(); this.connector.set("vectorLayer", null); this.mapModel.removeAsset(this.layer); @@ -386,49 +407,12 @@ define(["backbone", "models/connectors/GeoPoints-VectorData"], function ( this.addPoint({ latitude: point.get("latitude"), longitude: point.get("longitude"), + height: point.get("height"), + mapWidgetCoords: point.get("mapWidgetCoords"), }); } }, - /** - * The action to perform when the mode is "draw" and the user clicks on - * the map. - */ - handleDrawClick: function () { - if (!this.mode === "draw") return - const point = this.interactions.get("clickedPosition"); - if(!point) return - this.addPoint({ - latitude: point.get("latitude"), - longitude: point.get("longitude"), - }); - }, - - /** - * The action to perform when the mode is "move" and the user clicks on - * the map. - */ - handleMoveClick: function () { - if (!this.mode === "move") return - const feature = this.interactions.get("clickedFeature"); - if (!feature) return - // TODO: Set a listener to update the point feature and coords - // when it is clicked and dragged - }, - - /** - * The action to perform when the mode is "remove" and the user clicks on - * the map. - */ - handleRemoveClick: function () { - if (!this.mode === "remove") return - const feature = this.interactions.get("clickedFeature"); - if (!feature) return - // TODO: Get the coords of the clicked feature and remove the point - // from the polygon - console.log("remove feature", feature); - }, - /** * Clears the polygon that is being drawn */