1 | /// Map modules to control the view using the mouse. |
---|
2 | /** |
---|
3 | * \param map The map. |
---|
4 | */ |
---|
5 | webmap.modules.map.MouseViewControl = function(map) { |
---|
6 | this.map = map; |
---|
7 | this.hide_click = false; |
---|
8 | |
---|
9 | this.mouseDownListener = this.mouseDown.bind(this); |
---|
10 | this.mouseUpListener = this.mouseUp.bind(this); |
---|
11 | this.mouseMoveListener = this.mouseMove.bind(this); |
---|
12 | this.mouseWheelListener = this.mouseWheel.bind(this); |
---|
13 | this.mouseClickListener = this.mouseClick.bind(this); |
---|
14 | |
---|
15 | this.map.root.addEventListener("mousedown", this.mouseDownListener, false); |
---|
16 | this.map.root.addEventListener("mouseup", this.mouseUpListener, false); |
---|
17 | this.map.root.addEventListener("mouseleave", this.mouseUpListener, false); |
---|
18 | this.map.root.addEventListener("wheel", this.mouseWheelListener, false); |
---|
19 | this.map.root.addEventListener("mousewheel", this.mouseWheelListener, false); |
---|
20 | this.map.root.addEventListener("DOMMouseScroll", this.mouseWheelListener, false); |
---|
21 | this.map.root.addEventListener("click", this.mouseClickListener, true); |
---|
22 | } |
---|
23 | |
---|
24 | /// Destroy the module and clean up. |
---|
25 | webmap.modules.map.MouseViewControl.prototype.destroy = function() { |
---|
26 | this.map.root.removeEventListener("mousedown", this.mouseDownListener, false); |
---|
27 | this.map.root.removeEventListener("mouseup", this.mouseUpListener, false); |
---|
28 | this.map.root.removeEventListener("mousemove", this.mouseMoveListener, false); |
---|
29 | this.map.root.removeEventListener("mouseleave", this.mouseUpListener, false); |
---|
30 | this.map.root.removeEventListener("wheel", this.mouseWheelListener, false); |
---|
31 | this.map.root.removeEventListener("mousewheel", this.mousewheelListener, false); |
---|
32 | this.map.root.removeEventListener("DOMMouseScroll", this.mousewheelListener, false); |
---|
33 | this.map.root.removeEventListener("click", this.mouseClickListener, true); |
---|
34 | } |
---|
35 | |
---|
36 | /// Handle mouse down events. |
---|
37 | webmap.modules.map.MouseViewControl.prototype.mouseDown = function(event) { |
---|
38 | // Normalize the event and stop the default action. |
---|
39 | event = event || window.event; |
---|
40 | event.preventDefault(); |
---|
41 | |
---|
42 | // Remember the mouse coordinates. |
---|
43 | this.mouse_x = event.clientX; |
---|
44 | this.mouse_y = event.clientY; |
---|
45 | this.mouse_moved = false; |
---|
46 | this.map.root.addEventListener("mousemove", this.mouseMoveListener, false); |
---|
47 | } |
---|
48 | |
---|
49 | /// Handle mouse move events. |
---|
50 | webmap.modules.map.MouseViewControl.prototype.mouseMove = function(event) { |
---|
51 | // Skip the event if the mouse wasn't pressed on the map. |
---|
52 | if (this.mouse_x === undefined) return; |
---|
53 | |
---|
54 | // Normalize the event and prevent default actions. |
---|
55 | event = event || window.event; |
---|
56 | event.preventDefault(); |
---|
57 | |
---|
58 | // Scroll and remember the mouse coordinates. |
---|
59 | var diff_x = (event.clientX - this.mouse_x); |
---|
60 | var diff_y = (event.clientY - this.mouse_y); |
---|
61 | this.map.translate(diff_x, diff_y); |
---|
62 | this.mouse_x = event.clientX; |
---|
63 | this.mouse_y = event.clientY; |
---|
64 | this.mouse_moved = true; |
---|
65 | } |
---|
66 | |
---|
67 | /// Capture click events and shove them under the carpet. |
---|
68 | webmap.modules.map.MouseViewControl.prototype.mouseClick = function(event) { |
---|
69 | if (this.hide_click) { |
---|
70 | event = event || window.event; |
---|
71 | event.preventDefault(); |
---|
72 | event.stopPropagation(); |
---|
73 | this.hide_click = false; |
---|
74 | } |
---|
75 | } |
---|
76 | |
---|
77 | /// Handle mouse up events. |
---|
78 | webmap.modules.map.MouseViewControl.prototype.mouseUp = function(event) { |
---|
79 | // Skip the event if the mouse wasn't pressed on the map. |
---|
80 | if (this.mouse_x === undefined) return; |
---|
81 | |
---|
82 | // Normalize the event and prevent default actions. |
---|
83 | event = event || window.event; |
---|
84 | event.preventDefault(); |
---|
85 | |
---|
86 | // Scroll and clear the mouse coordinates. |
---|
87 | var diff_x = (event.clientX - this.mouse_x); |
---|
88 | var diff_y = (event.clientY - this.mouse_y); |
---|
89 | |
---|
90 | // If the mouse has moved since going down, it's not a click but a drag. |
---|
91 | this.hide_click = diff_x || diff_y || this.mouse_moved; |
---|
92 | |
---|
93 | this.map.translate(diff_x, diff_y); |
---|
94 | this.mouse_x = undefined; |
---|
95 | this.mouse_y = undefined; |
---|
96 | this.mouse_moved = undefined; |
---|
97 | this.map.root.removeEventListener("mousemove", this.mouseMoveListener, false); |
---|
98 | |
---|
99 | } |
---|
100 | |
---|
101 | /// Handle mouse scroll events. |
---|
102 | webmap.modules.map.MouseViewControl.prototype.mouseWheel = function(event) { |
---|
103 | event = event || window.event; |
---|
104 | event.preventDefault(); |
---|
105 | |
---|
106 | // Get the X and Y coordinates of the mouse within the SVG viewport. |
---|
107 | var bounds = this.map.getBoundingRect(); |
---|
108 | var x = event.clientX - bounds.left; |
---|
109 | var y = event.clientY - bounds.top; |
---|
110 | |
---|
111 | // Determine (or estimate) the amount of lines scrolled. |
---|
112 | var lines = 0; |
---|
113 | |
---|
114 | // Modern wheel events. |
---|
115 | if (event.deltaMode !== undefined && event.deltaY !== undefined) { |
---|
116 | switch (event.deltaMode) { |
---|
117 | case 0x00: lines = event.deltaY / 14; break; // About 14 pixels per line? |
---|
118 | case 0x01: lines = event.deltaY; break; // About exactly 1 line per line. |
---|
119 | case 0x02: lines = event.deltaY * 10; break; // Lets just call a page 10 lines. |
---|
120 | } |
---|
121 | // Old Gecko DOMMouseScroll events. |
---|
122 | } else if (event.detail) { |
---|
123 | switch (event.detail) { |
---|
124 | case 32768: lines = 10; break; // Lets just call a page 10 lines. |
---|
125 | case -32768: lines = -10; break; // Lets just call a page 10 lines. |
---|
126 | default: lines = event.detail; break; // About exactly 1 line per line. |
---|
127 | } |
---|
128 | // Old mousewheel events. |
---|
129 | } else if (event.wheelDelta) { |
---|
130 | lines = event.wheelDelta / -40.0; |
---|
131 | } |
---|
132 | |
---|
133 | // Handle the scroll. |
---|
134 | if (event.shiftKey) { |
---|
135 | this.map.rotate(5.0 * lines, this.map.canvas_width * 0.5, this.map.canvas_height * 0.5); // Rotate -5 degrees per line. |
---|
136 | } else { |
---|
137 | this.map.scale(Math.pow(1.15, -lines / 3.0), x, y); // Zoom 15% per 3 lines. |
---|
138 | } |
---|
139 | } |
---|
140 | |
---|
141 | |
---|
142 | /// Map modules to show the name of the currently selected robot in a HTML element. |
---|
143 | /** |
---|
144 | * The module works by replacing the text content of the given HTML element. |
---|
145 | * |
---|
146 | * \param map The map. |
---|
147 | * \param element The HTML element to display the name in. |
---|
148 | * \param default_msg The message to show when there is no selected robot. |
---|
149 | */ |
---|
150 | webmap.modules.map.ShowSelected = function(map, element, default_msg) { |
---|
151 | if (default_msg === undefined || default_msg === null) default_msg = "No selection"; |
---|
152 | |
---|
153 | this.map = map; |
---|
154 | this.default_msg = default_msg; |
---|
155 | this.element = element; |
---|
156 | this.onSelect(); |
---|
157 | } |
---|
158 | |
---|
159 | /// Destroy the module and clean up. |
---|
160 | webmap.modules.map.ShowSelected.prototype.destroy = function() { |
---|
161 | this.element.textContent = ""; |
---|
162 | } |
---|
163 | |
---|
164 | /// Handle selection changes by updating the text content of the DOM element. |
---|
165 | webmap.modules.map.ShowSelected.prototype.onSelect = function() { |
---|
166 | this.element.textContent = this.map.selected ? this.map.selected.name : this.default_msg; |
---|
167 | } |
---|
168 | |
---|
169 | |
---|
170 | /// Module to allow keyboard control of robots. |
---|
171 | /** |
---|
172 | * \param map The map. |
---|
173 | * \param speed The forward speed. |
---|
174 | * \param rotation_speed The rotational speed in rad/s. |
---|
175 | */ |
---|
176 | webmap.modules.map.Teleop = function(map, speed, rotation_speed) { |
---|
177 | this.map = map; |
---|
178 | this.speed = speed; |
---|
179 | this.rotation_speed = rotation_speed; |
---|
180 | |
---|
181 | // Initial keyboard state. |
---|
182 | this.forward = false; |
---|
183 | this.backward = false; |
---|
184 | this.left = false; |
---|
185 | this.right = false; |
---|
186 | |
---|
187 | // Timeout ID so we can cancel it later. |
---|
188 | this.timeout = null; |
---|
189 | |
---|
190 | // Set up event listeners. |
---|
191 | this.keydown_listener = this.keyEvent.bind(this, true); |
---|
192 | this.keyup_listener = this.keyEvent.bind(this, false); |
---|
193 | this.blur_listener = this.blur.bind(this); |
---|
194 | document.body.addEventListener("keydown", this.keydown_listener, false); |
---|
195 | document.body.addEventListener("keyup", this.keyup_listener, false); |
---|
196 | document.addEventListener("blur", this.blur_listener, false); |
---|
197 | } |
---|
198 | |
---|
199 | /// Destroy the module and clean up. |
---|
200 | webmap.modules.map.Teleop.prototype.destroy = function() { |
---|
201 | document.body.removeEventListener("keydown", this.keydown_listener, false); |
---|
202 | document.body.removeEventListener("keyup", this.keyup_listener, false); |
---|
203 | document.removeEventListener("blur", this.blur_listener, false); |
---|
204 | } |
---|
205 | |
---|
206 | /// Send the twist message based on the state of the keyboard. |
---|
207 | webmap.modules.map.Teleop.prototype.actuate = function() { |
---|
208 | if (this.map && this.map.selected) { |
---|
209 | var robot = this.map.selected; |
---|
210 | var x = 0; |
---|
211 | var rz = 0; |
---|
212 | if (this.forward) x += this.speed; |
---|
213 | if (this.backward) x -= this.speed; |
---|
214 | if (this.left) rz += this.rotation_speed; |
---|
215 | if (this.right) rz -= this.rotation_speed; |
---|
216 | if (x < 0) rz *= -1; |
---|
217 | this.map.selected.twist(x, 0, 0, 0, 0, rz); |
---|
218 | this.timeout = window.setTimeout(this.actuate.bind(this), 100); |
---|
219 | } |
---|
220 | } |
---|
221 | |
---|
222 | /// When the document loses focus, make sure we forget all pressed keys. |
---|
223 | webmap.modules.map.Teleop.prototype.blur = function() { |
---|
224 | // Reset keyboard state. |
---|
225 | this.forward = false; |
---|
226 | this.backward = false; |
---|
227 | this.left = false; |
---|
228 | this.right = false; |
---|
229 | |
---|
230 | // Cancel any running timeout. |
---|
231 | if (this.timeout !== null) { |
---|
232 | window.clearTimeout(this.timeout); |
---|
233 | this.timeout = null; |
---|
234 | } |
---|
235 | } |
---|
236 | |
---|
237 | /// Handle key events on the map. |
---|
238 | /** |
---|
239 | * \param down If true, the key has been pressed, otherwise it has been unpressed. |
---|
240 | * \param event The event that triggered the listener. |
---|
241 | */ |
---|
242 | webmap.modules.map.Teleop.prototype.keyEvent = function(down, event) { |
---|
243 | event = event || window.event; |
---|
244 | |
---|
245 | // Remember the state of interesting keys. |
---|
246 | var interested = false; |
---|
247 | switch (event.keyCode) { |
---|
248 | case 87: // W (87) means forwards. |
---|
249 | interested = true; |
---|
250 | this.forward = down; |
---|
251 | break; |
---|
252 | case 83: // S (83) means backwards. |
---|
253 | interested = true; |
---|
254 | this.backward = down; |
---|
255 | break; |
---|
256 | case 65: // A (65) means left. |
---|
257 | interested = true; |
---|
258 | this.left = down; |
---|
259 | break; |
---|
260 | case 68: // D (68) means right. |
---|
261 | interested = true; |
---|
262 | this.right = down; |
---|
263 | break; |
---|
264 | } |
---|
265 | |
---|
266 | // Start or stop the timeouts. |
---|
267 | if (this.forward || this.backward || this.left || this.right) { |
---|
268 | if (this.timeout === null) this.timeout = window.setTimeout(this.actuate.bind(this), 100); |
---|
269 | } else if (this.timeout !== null) { |
---|
270 | window.clearTimeout(this.timeout); |
---|
271 | this.timeout = null; |
---|
272 | } |
---|
273 | |
---|
274 | if (interested) event.preventDefault(); |
---|
275 | } |
---|
276 | |
---|
277 | /*////////////////// |
---|
278 | // Robot modules |
---|
279 | // This section of the file contains modules for robots. |
---|
280 | //////////////////*/ |
---|
281 | |
---|
282 | /// Create a new laser scanner sensor module. |
---|
283 | /** |
---|
284 | * \param robot The robot. |
---|
285 | * \param connection The ROS connection. |
---|
286 | * \param topic The name of the LaserScan topic. |
---|
287 | * \param x The X offset relative from robot origin. |
---|
288 | * \param y The Y offset relative from the robot origin. |
---|
289 | * \param angle The rotation of the sensor in degrees. |
---|
290 | * \param max_dots (Optional) Maximum number of dots to show. Defaults to 50. |
---|
291 | * \param throttle (Optional) The minimum time in milliseconds between receiving two updates. Defaults to 300. |
---|
292 | * |
---|
293 | * An angle of 0 degrees means the sensor is looking along the positive X axis of the robot. |
---|
294 | */ |
---|
295 | webmap.modules.robot.LaserScanner = function(robot, connection, topic, x, y, angle, max_dots, throttle) { |
---|
296 | if (max_dots === undefined || max_dots === null) max_dots = 50; |
---|
297 | if (throttle === undefined || throttle === null) throttle = 300; |
---|
298 | |
---|
299 | /// Locally unique ID. |
---|
300 | this.id = webmap.generateId(); |
---|
301 | |
---|
302 | this.robot = robot; |
---|
303 | this.connection = connection; |
---|
304 | this.topic = topic; |
---|
305 | this.svg = document.createElementNS(webmap.svgns, "g"); |
---|
306 | this.svg.setAttribute("class", "laserscan"); |
---|
307 | this.dots = []; |
---|
308 | |
---|
309 | this.x = x; |
---|
310 | this.y = y; |
---|
311 | this.angle = angle; |
---|
312 | |
---|
313 | // Create the dots to visualize the readings. |
---|
314 | for (var i = 0; i < max_dots; ++i) { |
---|
315 | var dot = document.createElementNS(webmap.svgns, "circle"); |
---|
316 | dot.setAttribute("cx", 0); |
---|
317 | dot.setAttribute("cy", 0); |
---|
318 | dot.setAttribute("r", 0.05); |
---|
319 | dot.setAttribute("class", "dot"); |
---|
320 | dot.style.display = "none"; |
---|
321 | dot = this.svg.appendChild(dot); |
---|
322 | this.dots.push(dot); |
---|
323 | } |
---|
324 | |
---|
325 | // Add the SVG. |
---|
326 | this.robot.svg.appendChild(this.svg); |
---|
327 | |
---|
328 | // Subsribe to the data topic. |
---|
329 | this.connection.subscribe(this.handleData.bind(this), this.topic, null, throttle, null, null, null, this.id); |
---|
330 | } |
---|
331 | |
---|
332 | /// Destroy the module and clean up. |
---|
333 | webmap.modules.robot.LaserScanner.prototype.destroy = function() { |
---|
334 | this.connection.unsubscribe(this.topic, this.id); |
---|
335 | this.svg.parentNode.removeChild(this.svg); |
---|
336 | } |
---|
337 | |
---|
338 | /// Handle sensor data by updating the drawing. |
---|
339 | webmap.modules.robot.LaserScanner.prototype.handleData = function(msg) { |
---|
340 | // Add a dot for all valid readings. |
---|
341 | var count = Math.min(this.dots.length, msg.ranges.length); |
---|
342 | |
---|
343 | for (var i = 0; i < this.dots.length; ++i) { |
---|
344 | // If a dot has no reading, make it invisible. |
---|
345 | if (i >= count) { |
---|
346 | this.dots[i].style.display = "none"; |
---|
347 | continue; |
---|
348 | } |
---|
349 | |
---|
350 | var index = Math.round(i * msg.ranges.length / count); |
---|
351 | var angle = ((msg.angle_min + index * msg.angle_increment) / Math.PI * 180 + 360) % 360; |
---|
352 | var range = msg.ranges[index]; |
---|
353 | |
---|
354 | // Don't render invalid readings. |
---|
355 | if (range < msg.range_min || range > msg.range_max) { |
---|
356 | this.dots[i].style.display = "none"; |
---|
357 | continue; |
---|
358 | } |
---|
359 | |
---|
360 | // Place the dot. |
---|
361 | var transform = webmap.svg.createSVGMatrix().rotate(angle).translate(range, 0); |
---|
362 | this.dots[i].transform.baseVal.initialize(this.dots[i].transform.baseVal.createSVGTransformFromMatrix(transform)); |
---|
363 | this.dots[i].style.display = "inline"; |
---|
364 | } |
---|
365 | } |
---|