diff --git a/src/js/collections/maps/GeoPoints.js b/src/js/collections/maps/GeoPoints.js index 25cf9569b..48ee17555 100644 --- a/src/js/collections/maps/GeoPoints.js +++ b/src/js/collections/maps/GeoPoints.js @@ -131,6 +131,95 @@ define(["backbone", "models/maps/GeoPoint"], function (Backbone, GeoPoint) { }; }, + // 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"; + } + const czml = [this.getCZMLHeader()]; + switch (geometryType) { + case "Point": + 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(); + // make a random ID: + const id = "polygon_" + Math.floor(Math.random() * 1000000); + return { + id: id, + 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 @@ -208,6 +297,18 @@ define(["backbone", "models/maps/GeoPoint"], function (Backbone, GeoPoint) { 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(); + }); + }, } ); diff --git a/src/js/models/connectors/GeoPoints-VectorData.js b/src/js/models/connectors/GeoPoints-VectorData.js index 02bc732b4..95a7a9b3a 100644 --- a/src/js/models/connectors/GeoPoints-VectorData.js +++ b/src/js/models/connectors/GeoPoints-VectorData.js @@ -159,9 +159,11 @@ define([ updateVectorLayer: function () { const points = this.get("points") || this.setPoints(); const layer = this.get("vectorLayer") || this.setVectorLayer(); - const geoJson = points.toGeoJson("Polygon"); + const type = model.get("type"); + const geom = "Polygon"; + const data = type === "geojson" ? points.toGeoJson(geom) : this.toCzml(geom); const opts = layer.getCesiumOptions() || {}; - opts.data = geoJson; + opts.data = data; layer.set("cesiumOptions", opts); }, } diff --git a/src/js/models/maps/GeoPoint.js b/src/js/models/maps/GeoPoint.js index 7626864da..2ff0d7415 100644 --- a/src/js/models/maps/GeoPoint.js +++ b/src/js/models/maps/GeoPoint.js @@ -1,6 +1,6 @@ "use strict"; -define(["backbone"], function (Backbone) { +define(["backbone", "models/maps/GeoUtilities"], function (Backbone, GeoUtilities) { /** * @class GeoPoint * @classdesc The GeoPoint model stores geographical coordinates including @@ -68,6 +68,43 @@ define(["backbone"], function (Backbone) { }; }, + /** + * 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 diff --git a/src/js/models/maps/GeoUtilities.js b/src/js/models/maps/GeoUtilities.js new file mode 100644 index 000000000..4ea00f806 --- /dev/null +++ b/src/js/models/maps/GeoUtilities.js @@ -0,0 +1,58 @@ +"use strict"; + +define(["backbone", "models/maps/GeoUtilities"], function ( + Backbone, + GeoUtilities +) { + /** + * @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..24372d065 100644 --- a/src/js/models/maps/Geohash.js +++ b/src/js/models/maps/Geohash.js @@ -287,7 +287,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. @@ -346,24 +346,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.geodeticToECEF(coord); }, } ); diff --git a/src/js/views/maps/DrawToolView.js b/src/js/views/maps/DrawToolView.js index 3700780b4..75fa7dc17 100644 --- a/src/js/views/maps/DrawToolView.js +++ b/src/js/views/maps/DrawToolView.js @@ -31,9 +31,51 @@ define(["backbone", "models/connectors/GeoPoints-VectorData"], function ( className: "draw-tool", /** - * The current mode of the draw tool. This could be "draw", "edit", - * "delete", or false to indicate that the draw tool is not active. - * Currently only "draw" and false are supported. + * Class to use for the buttons + * @type {string} + */ + buttonClass: "map-view__button", + + /** + * The buttons to display in the toolbar and their corresponding actions. + * TODO: Finish documenting this when more finalized. + */ + buttons: [ + { + name: "draw", // === mode + label: "Draw Polygon", + icon: "pencil", + }, + { + name: "move", + label: "Move Point", + icon: "move", + }, + { + name: "remove", + label: "Remove Point", + icon: "eraser", + }, + { + name: "clear", + label: "Clear Polygon", + icon: "trash", + method: "clearPoints", + }, + { + name: "save", + label: "Save", + icon: "save", + method: "save", + }, + ], + + buttonEls: {}, + + /** + * The current mode of the draw tool. This can be "draw", "move", + * "remove", or "add" - any of the "name" properties of the buttons array, + * excluding buttons like "clear" and "save" that have a method property. */ mode: false, @@ -106,8 +148,19 @@ define(["backbone", "models/connectors/GeoPoints-VectorData"], function ( */ setUpLayer: function () { this.layer = this.mapModel.addAsset({ - type: "GeoJsonDataSource", - hideInLayerList: true, // <- TODO: Hide in LayerList, doc in mapConfig + type: "CzmlDataSource", + 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 + colorPalette: { + colors: [ + { + color: "#FF3E41", // TODO + }, + ], + }, }); return this.layer; }, @@ -134,14 +187,14 @@ define(["backbone", "models/connectors/GeoPoints-VectorData"], function ( * @returns {GeoPoint} The GeoPoint model that was added to the polygon. */ addPoint: function (point) { - return this.points.addPoint(point); + return this.points?.addPoint(point); }, /** * Clears the polygon that is being drawn. */ clearPoints: function () { - this.points.reset(null); + this.points?.reset(null); }, /** @@ -180,24 +233,45 @@ define(["backbone", "models/connectors/GeoPoints-VectorData"], function ( renderToolbar: function () { const view = this; const el = this.el; - const drawButton = document.createElement("button"); - drawButton.innerHTML = "Draw"; - drawButton.addEventListener("click", function () { - if (view.mode === "draw") { - view.setMode(false); - } else { - view.setMode("draw"); - } - }); - this.drawButton = drawButton; - el.appendChild(drawButton); - const clearButton = document.createElement("button"); - clearButton.innerHTML = "Clear"; - clearButton.addEventListener("click", function () { - view.clearPoints(); - view.setMode(false); + + // Create the buttons + view.buttons.forEach(options => { + const button = document.createElement("button"); + button.className = this.buttonClass; + button.innerHTML = ` ${options.label}`; + button.addEventListener("click", function () { + const method = options.method; + if(method) view[method](); + else view.toggleMode(options.name); + }); + if(!view.buttonEls) view.buttonEls = {}; + view.buttonEls[options.name + "Button"] = button; + el.appendChild(button); }); - el.appendChild(clearButton); + }, + + /** + * Sends the polygon coordinates to a callback function to do something + * with them. + * TODO: This is a WIP. + */ + save: function () { + this.setMode(false); + this.removeClickListeners(); + console.log(this.points.toJSON()); + // TODO: Call a callback function to save the polygon + }, + + /** + * Toggles the mode of the draw tool. + * @param {string} mode - The mode to toggle to. + */ + toggleMode: function (mode) { + if (this.mode === mode) { + this.setMode(false); + } else { + this.setMode(mode); + } }, /** @@ -209,12 +283,34 @@ define(["backbone", "models/connectors/GeoPoints-VectorData"], function ( setMode: function (mode) { if (this.mode === mode) return; this.mode = mode; - if (mode === "draw") { - this.setClickListeners(); - this.drawButton.style.backgroundColor = "green"; - } else if (mode === false) { + if (mode) { + if (!this.listeningForClicks) this.setClickListeners(); + this.activateButton(mode); + } else { + this.resetButtonStyles(); this.removeClickListeners(); - this.drawButton.style.backgroundColor = "grey"; + } + }, + + /** + * Sets the style of the button with the given name to indicate that it is + * active. + */ + activateButton: function (buttonName) { + const buttonEl = this.buttonEls[buttonName + "Button"]; + if(!buttonEl) return; + this.resetButtonStyles(); + buttonEl.style.backgroundColor = "blue"; // TODO - create active style + }, + + /** + * Resets the styles of all of the buttons to indicate that they are not + * active. + */ + resetButtonStyles: function () { + // Iterate through the buttonEls object and reset the styles + for (const button in this.buttonEls) { + this.buttonEls[button].style.backgroundColor = "grey"; // TODO - create default style } }, @@ -287,7 +383,6 @@ define(["backbone", "models/connectors/GeoPoints-VectorData"], function ( // Add the point to the polygon if (this.mode === "draw") { const point = this.interactions.get("clickedPosition"); - console.log("Adding point", point); this.addPoint({ latitude: point.get("latitude"), longitude: point.get("longitude"), @@ -295,6 +390,45 @@ define(["backbone", "models/connectors/GeoPoints-VectorData"], function ( } }, + /** + * 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 */ diff --git a/test/config/tests.json b/test/config/tests.json index a558e0dec..a668b5fd9 100644 --- a/test/config/tests.json +++ b/test/config/tests.json @@ -23,6 +23,9 @@ "./js/specs/unit/models/metadata/eml211/EMLDistribution.spec.js", "./js/specs/unit/models/maps/assets/CesiumImagery.spec.js", "./js/specs/unit/collections/maps/Geohashes.spec.js", + "./js/specs/unit/models/maps/GeoPoint.spec.js", + "./js/specs/unit/models/maps/GeoScale.spec.js", + "./js/specs/unit/models/maps/MapInteraction.spec.js", "./js/specs/unit/models/connectors/Filters-Map.spec.js", "./js/specs/unit/models/connectors/Filters-Search.spec.js", "./js/specs/unit/models/connectors/Map-Search-Filters.spec.js",