/// Map modules to control the view using the mouse. /** * \param map The map. */ webmap.modules.map.MouseViewControl = function(map) { this.map = map; this.hide_click = false; this.mouseDownListener = this.mouseDown.bind(this); this.mouseUpListener = this.mouseUp.bind(this); this.mouseMoveListener = this.mouseMove.bind(this); this.mouseWheelListener = this.mouseWheel.bind(this); this.mouseClickListener = this.mouseClick.bind(this); this.map.root.addEventListener("mousedown", this.mouseDownListener, false); this.map.root.addEventListener("mouseup", this.mouseUpListener, false); this.map.root.addEventListener("mouseleave", this.mouseUpListener, false); this.map.root.addEventListener("wheel", this.mouseWheelListener, false); this.map.root.addEventListener("mousewheel", this.mouseWheelListener, false); this.map.root.addEventListener("DOMMouseScroll", this.mouseWheelListener, false); this.map.root.addEventListener("click", this.mouseClickListener, true); } /// Destroy the module and clean up. webmap.modules.map.MouseViewControl.prototype.destroy = function() { this.map.root.removeEventListener("mousedown", this.mouseDownListener, false); this.map.root.removeEventListener("mouseup", this.mouseUpListener, false); this.map.root.removeEventListener("mousemove", this.mouseMoveListener, false); this.map.root.removeEventListener("mouseleave", this.mouseUpListener, false); this.map.root.removeEventListener("wheel", this.mouseWheelListener, false); this.map.root.removeEventListener("mousewheel", this.mousewheelListener, false); this.map.root.removeEventListener("DOMMouseScroll", this.mousewheelListener, false); this.map.root.removeEventListener("click", this.mouseClickListener, true); } /// Handle mouse down events. webmap.modules.map.MouseViewControl.prototype.mouseDown = function(event) { // Normalize the event and stop the default action. event = event || window.event; event.preventDefault(); // Remember the mouse coordinates. this.mouse_x = event.clientX; this.mouse_y = event.clientY; this.mouse_moved = false; this.map.root.addEventListener("mousemove", this.mouseMoveListener, false); } /// Handle mouse move events. webmap.modules.map.MouseViewControl.prototype.mouseMove = function(event) { // Skip the event if the mouse wasn't pressed on the map. if (this.mouse_x === undefined) return; // Normalize the event and prevent default actions. event = event || window.event; event.preventDefault(); // Scroll and remember the mouse coordinates. var diff_x = (event.clientX - this.mouse_x); var diff_y = (event.clientY - this.mouse_y); this.map.translate(diff_x, diff_y); this.mouse_x = event.clientX; this.mouse_y = event.clientY; this.mouse_moved = true; } /// Capture click events and shove them under the carpet. webmap.modules.map.MouseViewControl.prototype.mouseClick = function(event) { if (this.hide_click) { event = event || window.event; event.preventDefault(); event.stopPropagation(); this.hide_click = false; } } /// Handle mouse up events. webmap.modules.map.MouseViewControl.prototype.mouseUp = function(event) { // Skip the event if the mouse wasn't pressed on the map. if (this.mouse_x === undefined) return; // Normalize the event and prevent default actions. event = event || window.event; event.preventDefault(); // Scroll and clear the mouse coordinates. var diff_x = (event.clientX - this.mouse_x); var diff_y = (event.clientY - this.mouse_y); // If the mouse has moved since going down, it's not a click but a drag. this.hide_click = diff_x || diff_y || this.mouse_moved; this.map.translate(diff_x, diff_y); this.mouse_x = undefined; this.mouse_y = undefined; this.mouse_moved = undefined; this.map.root.removeEventListener("mousemove", this.mouseMoveListener, false); } /// Handle mouse scroll events. webmap.modules.map.MouseViewControl.prototype.mouseWheel = function(event) { event = event || window.event; event.preventDefault(); // Get the X and Y coordinates of the mouse within the SVG viewport. var bounds = this.map.getBoundingRect(); var x = event.clientX - bounds.left; var y = event.clientY - bounds.top; // Determine (or estimate) the amount of lines scrolled. var lines = 0; // Modern wheel events. if (event.deltaMode !== undefined && event.deltaY !== undefined) { switch (event.deltaMode) { case 0x00: lines = event.deltaY / 14; break; // About 14 pixels per line? case 0x01: lines = event.deltaY; break; // About exactly 1 line per line. case 0x02: lines = event.deltaY * 10; break; // Lets just call a page 10 lines. } // Old Gecko DOMMouseScroll events. } else if (event.detail) { switch (event.detail) { case 32768: lines = 10; break; // Lets just call a page 10 lines. case -32768: lines = -10; break; // Lets just call a page 10 lines. default: lines = event.detail; break; // About exactly 1 line per line. } // Old mousewheel events. } else if (event.wheelDelta) { lines = event.wheelDelta / -40.0; } // Handle the scroll. if (event.shiftKey) { this.map.rotate(5.0 * lines, this.map.canvas_width * 0.5, this.map.canvas_height * 0.5); // Rotate -5 degrees per line. } else { this.map.scale(Math.pow(1.15, -lines / 3.0), x, y); // Zoom 15% per 3 lines. } } /// Map modules to show the name of the currently selected robot in a HTML element. /** * The module works by replacing the text content of the given HTML element. * * \param map The map. * \param element The HTML element to display the name in. * \param default_msg The message to show when there is no selected robot. */ webmap.modules.map.ShowSelected = function(map, element, default_msg) { if (default_msg === undefined || default_msg === null) default_msg = "No selection"; this.map = map; this.default_msg = default_msg; this.element = element; this.onSelect(); } /// Destroy the module and clean up. webmap.modules.map.ShowSelected.prototype.destroy = function() { this.element.textContent = ""; } /// Handle selection changes by updating the text content of the DOM element. webmap.modules.map.ShowSelected.prototype.onSelect = function() { this.element.textContent = this.map.selected ? this.map.selected.name : this.default_msg; } /// Module to allow keyboard control of robots. /** * \param map The map. * \param speed The forward speed. * \param rotation_speed The rotational speed in rad/s. */ webmap.modules.map.Teleop = function(map, speed, rotation_speed) { this.map = map; this.speed = speed; this.rotation_speed = rotation_speed; // Initial keyboard state. this.forward = false; this.backward = false; this.left = false; this.right = false; // Timeout ID so we can cancel it later. this.timeout = null; // Set up event listeners. this.keydown_listener = this.keyEvent.bind(this, true); this.keyup_listener = this.keyEvent.bind(this, false); this.blur_listener = this.blur.bind(this); document.body.addEventListener("keydown", this.keydown_listener, false); document.body.addEventListener("keyup", this.keyup_listener, false); document.addEventListener("blur", this.blur_listener, false); } /// Destroy the module and clean up. webmap.modules.map.Teleop.prototype.destroy = function() { document.body.removeEventListener("keydown", this.keydown_listener, false); document.body.removeEventListener("keyup", this.keyup_listener, false); document.removeEventListener("blur", this.blur_listener, false); } /// Send the twist message based on the state of the keyboard. webmap.modules.map.Teleop.prototype.actuate = function() { if (this.map && this.map.selected) { var robot = this.map.selected; var x = 0; var rz = 0; if (this.forward) x += this.speed; if (this.backward) x -= this.speed; if (this.left) rz += this.rotation_speed; if (this.right) rz -= this.rotation_speed; if (x < 0) rz *= -1; this.map.selected.twist(x, 0, 0, 0, 0, rz); this.timeout = window.setTimeout(this.actuate.bind(this), 100); } } /// When the document loses focus, make sure we forget all pressed keys. webmap.modules.map.Teleop.prototype.blur = function() { // Reset keyboard state. this.forward = false; this.backward = false; this.left = false; this.right = false; // Cancel any running timeout. if (this.timeout !== null) { window.clearTimeout(this.timeout); this.timeout = null; } } /// Handle key events on the map. /** * \param down If true, the key has been pressed, otherwise it has been unpressed. * \param event The event that triggered the listener. */ webmap.modules.map.Teleop.prototype.keyEvent = function(down, event) { event = event || window.event; // Remember the state of interesting keys. var interested = false; switch (event.keyCode) { case 87: // W (87) means forwards. interested = true; this.forward = down; break; case 83: // S (83) means backwards. interested = true; this.backward = down; break; case 65: // A (65) means left. interested = true; this.left = down; break; case 68: // D (68) means right. interested = true; this.right = down; break; } // Start or stop the timeouts. if (this.forward || this.backward || this.left || this.right) { if (this.timeout === null) this.timeout = window.setTimeout(this.actuate.bind(this), 100); } else if (this.timeout !== null) { window.clearTimeout(this.timeout); this.timeout = null; } if (interested) event.preventDefault(); } /*////////////////// // Robot modules // This section of the file contains modules for robots. //////////////////*/ /// Create a new laser scanner sensor module. /** * \param robot The robot. * \param connection The ROS connection. * \param topic The name of the LaserScan topic. * \param x The X offset relative from robot origin. * \param y The Y offset relative from the robot origin. * \param angle The rotation of the sensor in degrees. * \param max_dots (Optional) Maximum number of dots to show. Defaults to 50. * \param throttle (Optional) The minimum time in milliseconds between receiving two updates. Defaults to 300. * * An angle of 0 degrees means the sensor is looking along the positive X axis of the robot. */ webmap.modules.robot.LaserScanner = function(robot, connection, topic, x, y, angle, max_dots, throttle) { if (max_dots === undefined || max_dots === null) max_dots = 50; if (throttle === undefined || throttle === null) throttle = 300; /// Locally unique ID. this.id = webmap.generateId(); this.robot = robot; this.connection = connection; this.topic = topic; this.svg = document.createElementNS(webmap.svgns, "g"); this.svg.setAttribute("class", "laserscan"); this.dots = []; this.x = x; this.y = y; this.angle = angle; // Create the dots to visualize the readings. for (var i = 0; i < max_dots; ++i) { var dot = document.createElementNS(webmap.svgns, "circle"); dot.setAttribute("cx", 0); dot.setAttribute("cy", 0); dot.setAttribute("r", 0.05); dot.setAttribute("class", "dot"); dot.style.display = "none"; dot = this.svg.appendChild(dot); this.dots.push(dot); } // Add the SVG. this.robot.svg.appendChild(this.svg); // Subsribe to the data topic. this.connection.subscribe(this.handleData.bind(this), this.topic, null, throttle, null, null, null, this.id); } /// Destroy the module and clean up. webmap.modules.robot.LaserScanner.prototype.destroy = function() { this.connection.unsubscribe(this.topic, this.id); this.svg.parentNode.removeChild(this.svg); } /// Handle sensor data by updating the drawing. webmap.modules.robot.LaserScanner.prototype.handleData = function(msg) { // Add a dot for all valid readings. var count = Math.min(this.dots.length, msg.ranges.length); for (var i = 0; i < this.dots.length; ++i) { // If a dot has no reading, make it invisible. if (i >= count) { this.dots[i].style.display = "none"; continue; } var index = Math.round(i * msg.ranges.length / count); var angle = ((msg.angle_min + index * msg.angle_increment) / Math.PI * 180 + 360) % 360; var range = msg.ranges[index]; // Don't render invalid readings. if (range < msg.range_min || range > msg.range_max) { this.dots[i].style.display = "none"; continue; } // Place the dot. var transform = webmap.svg.createSVGMatrix().rotate(angle).translate(range, 0); this.dots[i].transform.baseVal.initialize(this.dots[i].transform.baseVal.createSVGTransformFromMatrix(transform)); this.dots[i].style.display = "inline"; } }