"use strict";
define([
"backbone",
"models/connectors/GeoPoints-CesiumPolygon",
"models/connectors/GeoPoints-CesiumPoints",
"collections/maps/GeoPoints",
], function (Backbone, GeoPointsVectorData, GeoPointsCesiumPoints, GeoPoints) {
/**
* @class DrawTool
* @classdesc The DrawTool view allows a user to draw an arbitrary polygon on
* the map. The polygon is stored in a GeoPoints collection and displayed on
* the map using a connected CesiumVectorData model.
* @classcategory Views/Maps
* @name DrawTool
* @extends Backbone.View
* @screenshot views/maps/DrawTool.png
* @since 2.27.0
* @constructs DrawTool
*/
var DrawTool = Backbone.View.extend(
/** @lends DrawTool.prototype */ {
/**
* The type of View this is
* @type {string}
*/
type: "DrawTool",
/**
* The HTML classes to use for this view's element
* @type {string}
*/
className: "draw-tool",
/**
* Class to use for the buttons
* @type {string}
*/
buttonClass: "map-view__button",
/**
* Class to use for the active button
* @type {string}
*/
buttonClassActive: "map-view__button--active",
/**
* @typedef {Object} DrawToolButtonOptions
* @property {string} name - The name of the button. This should be the
* same as the mode that the button will activate (if the button is
* supposed to activate a mode).
* @property {string} label - The label to display on the button.
* @property {string} icon - The name of the icon to display on the
* button.
* @property {string} [method] - The name of the method to call when the
* button is clicked. If this is not provided, the button will toggle the
* mode of the draw tool.
*/
/**
* The buttons to display in the toolbar and their corresponding actions.
* @type {DrawToolButtonOptions[]}
*/
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: "reset",
},
{
name: "save",
label: "Save",
icon: "save",
method: "save",
},
],
/**
* The buttons that have been rendered in the toolbar. Formatted as an
* object with the button name as the key and the button element as the
* value.
* @type {Object}
*/
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,
/**
* The Cesium map model to draw on. This must be the same model that the
* mapWidget is using.
* @type {Map}
*/
mapModel: undefined,
/**
* A reference to the MapInteraction model on the MapModel that is used to
* listen for clicks on the map.
* @type {MapInteraction}
*/
interactions: undefined,
/**
* The CesiumVectorData model that will display the polygon that is being
* drawn.
* @type {CesiumVectorData}
*/
layer: undefined,
/**
* The GeoPoints collection that stores the points of the polygon that is
* being drawn.
* @type {GeoPoints}
*/
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.3,
/**
* Initializes the DrawTool
* @param {Object} options - A literal object with options to pass to the
* view
* @param {Map} options.model - The Cesium map model to draw on. This must
* be the same model that the mapWidget is using.
* @param {string} [options.mode=false] - The initial mode of the draw
* tool.
*/
initialize: function (options) {
this.mapModel = options.model;
if (!this.mapModel) {
console.warn("No map model was provided.");
return;
}
// Add models & collections and add interactions, layer, connector,
// points, and originalAction properties to this view
this.setUpMapModel();
this.setUpLayer();
this.setUpConnectors();
},
/**
* Sets up the map model and adds the interactions and originalAction
* properties to this view.
*/
setUpMapModel: function () {
this.originalAction = this.mapModel.get("clickFeatureAction");
this.interactions =
this.mapModel.get("interactions") ||
this.mapModel.setUpInteractions();
},
/**
* Sets up the layer to show the polygon on the map that is being drawn.
* Adds the layer property to this view.
* @returns {CesiumVectorData} The CesiumVectorData model that will
* display the polygon that is being drawn.
*/
setUpLayer: function () {
this.layer = this.mapModel.addAsset({
type: "CustomDataSource",
label: "Your Polygon",
description: "The polygon that you are drawing on the map",
hideInLayerList: true,
outlineColor: this.color,
highlightColor: this.color,
opacity: this.opacity,
colorPalette: {
colors: [
{
color: this.color,
},
],
},
});
},
/**
* Sets up the connector to connect the GeoPoints collection to the
* CesiumVectorData model. Adds the connector and points properties to
* this view.
* @returns {GeoPointsVectorData} The connector
*/
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.polygonConnector.connect();
this.pointsConnector.connect();
return this.connector;
},
/**
* Adds a point to the polygon that is being drawn.
* @param {Object} point - The point to add to the polygon. This should
* have a latitude and longitude property.
* @returns {GeoPoint} The GeoPoint model that was added to the polygon.
*/
addPoint: function (point) {
return this.points?.addPoint(point);
},
/**
* Clears the polygon that is being drawn.
*/
clearPoints: function () {
this.points?.reset(null);
},
/**
* Resets the draw tool to its initial state.
*/
reset: function () {
this.setMode(false);
this.clearPoints();
this.removeClickListeners();
},
/**
* Removes the polygon object from the map
*/
removeLayer: function () {
if (!this.mapModel || !this.layer) return;
this.polygonConnector.disconnect();
this.polygonConnector.set("vectorLayer", null);
this.pointsConnector.disconnect();
this.pointsConnector.set("vectorLayer", null);
this.mapModel.removeAsset(this.layer);
},
/**
* Renders the DrawTool
* @returns {DrawTool} Returns the view
*/
render: function () {
if (!this.mapModel) {
this.showError("No map model was provided.");
return this;
}
this.renderToolbar();
return this;
},
/**
* Show an error message to the user if the map model is not available
* or any other error occurs.
* @param {string} [message] - The error message to show to the user.
*/
showError: function (message) {
const str =
`<i class="icon-warning-sign icon-left"></i>` +
`<span> The draw tool is not available. ${message}</span>`;
this.el.innerHTML = str;
},
/**
* Create and insert the buttons for drawing and clearing the polygon.
*/
renderToolbar: function () {
const view = this;
const el = this.el;
// Create the buttons
view.buttons.forEach((options) => {
const button = document.createElement("button");
button.className = this.buttonClass;
button.innerHTML = `<i class="icon icon-${options.icon}"></i> ${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);
});
},
/**
* Sends the polygon coordinates to a callback function to do something
* with them.
* @param {Function} callback - The callback function to send the polygon
* coordinates to.
*/
save: function (callback) {
this.setMode(false);
if (callback && typeof callback === "function") {
callback(this.points.toJSON());
}
},
/**
* 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);
}
},
/**
* Sets the mode of the draw tool. Currently only "draw" and false are
* supported.
* @param {string|boolean} mode - The mode to set. This can be "draw" or
* false to indicate that the draw tool should not be active.
*/
setMode: function (mode) {
if (this.mode === mode) return;
this.mode = mode;
if (mode) {
if (!this.listeningForClicks) this.setClickListeners();
this.activateButton(mode);
} else {
this.resetButtonStyles();
this.removeClickListeners();
}
},
/**
* 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.classList.add(this.buttonClassActive);
},
/**
* 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) {
if (this.buttonEls.hasOwnProperty(button)) {
const buttonEl = this.buttonEls[button];
buttonEl.classList.remove(this.buttonClassActive);
}
}
},
/**
* Removes the click listeners from the map model and sets the
* clickFeatureAction back to its original value.
*/
removeClickListeners: function () {
const handler = this.clickHandler;
const originalAction = this.originalAction;
if (handler) {
handler.stopListening();
handler.clear();
this.clickHandler = null;
}
this.mapModel.set("clickFeatureAction", originalAction);
this.listeningForClicks = false;
},
/**
* Set listeners to call the handleClick method when the user clicks on
* the map.
*/
setClickListeners: function () {
const view = this;
const handler = (this.clickHandler = new Backbone.Model());
const interactions = this.interactions;
const clickedPosition = interactions.get("clickedPosition");
this.mapModel.set("clickFeatureAction", null);
handler.listenTo(
clickedPosition,
"change:latitude change:longitude",
() => {
view.handleClick();
},
);
this.listeningForClicks = true;
// When the clickedPosition GeoPoint model or the MapInteractions model
// is replaced, restart the listeners on the new model.
handler.listenToOnce(
interactions,
"change:clickedPosition",
function () {
if (view.listeningForClicks) {
view.handleClick();
view.setClickListeners();
}
},
);
handler.listenToOnce(this.mapModel, "change:interactions", function () {
if (view.listeningForClicks) {
view.handleClick();
view.setClickListeners();
}
});
},
/**
* Handles a click on the map. If the draw tool is active, it will add the
* coordinates of the click to the polygon being drawn.
* @param {Number} [throttle=50] - The number of milliseconds to block
* clicks for after a click is handled. This prevents double clicks.
*/
handleClick: function (throttle = 50) {
// Prevent double clicks
if (this.clickActionBlocked) return;
this.clickActionBlocked = true;
setTimeout(() => {
this.clickActionBlocked = false;
}, throttle);
// Add the point to the polygon
if (this.mode === "draw") {
const point = this.interactions.get("clickedPosition");
this.addPoint({
latitude: point.get("latitude"),
longitude: point.get("longitude"),
height: point.get("height"),
mapWidgetCoords: point.get("mapWidgetCoords"),
});
}
},
/**
* Clears the polygon that is being drawn
*/
onClose: function () {
this.removeLayer();
this.removeClickListeners();
},
},
);
return DrawTool;
});