X-Git-Url: https://defiant.homedns.org/gitweb/?p=ros_wild_thumper.git;a=blobdiff_plain;f=www%2Fassets%2Fjavascripts%2Fros2d.js;fp=www%2Fassets%2Fjavascripts%2Fros2d.js;h=d5c433e4288d8212334a1e349db884417057bde3;hp=0000000000000000000000000000000000000000;hb=01848a9a84b296519ca4b4f41906159fd4158213;hpb=e1f7a5148b9999b57a91c1ddb244397bf841de7e diff --git a/www/assets/javascripts/ros2d.js b/www/assets/javascripts/ros2d.js new file mode 100644 index 0000000..d5c433e --- /dev/null +++ b/www/assets/javascripts/ros2d.js @@ -0,0 +1,1186 @@ +/** + * @author Russell Toris - rctoris@wpi.edu + */ + +var ROS2D = ROS2D || { + REVISION : '0.9.0' +}; + +// convert the given global Stage coordinates to ROS coordinates +createjs.Stage.prototype.globalToRos = function(x, y) { + var rosX = (x - this.x) / this.scaleX; + var rosY = (this.y - y) / this.scaleY; + return new ROSLIB.Vector3({ + x : rosX, + y : rosY + }); +}; + +// convert the given ROS coordinates to global Stage coordinates +createjs.Stage.prototype.rosToGlobal = function(pos) { + var x = pos.x * this.scaleX + this.x; + var y = pos.y * this.scaleY + this.y; + return { + x : x, + y : y + }; +}; + +// convert a ROS quaternion to theta in degrees +createjs.Stage.prototype.rosQuaternionToGlobalTheta = function(orientation) { + // See https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles#Rotation_matrices + // here we use [x y z] = R * [1 0 0] + var q0 = orientation.w; + var q1 = orientation.x; + var q2 = orientation.y; + var q3 = orientation.z; + // Canvas rotation is clock wise and in degrees + return -Math.atan2(2 * (q0 * q3 + q1 * q2), 1 - 2 * (q2 * q2 + q3 * q3)) * 180.0 / Math.PI; +}; + +/** + * @author Russell Toris - rctoris@wpi.edu + */ + +/** + * An image map is a PNG image scaled to fit to the dimensions of a OccupancyGrid. + * + * @constructor + * @param options - object with following keys: + * * message - the occupancy grid map meta data message + * * image - the image URL to load + */ +ROS2D.ImageMap = function(options) { + options = options || {}; + var message = options.message; + var image = options.image; + + // save the metadata we need + this.pose = new ROSLIB.Pose({ + position : message.origin.position, + orientation : message.origin.orientation + }); + + // set the size + this.width = message.width; + this.height = message.height; + + // create the bitmap + createjs.Bitmap.call(this, image); + // change Y direction + this.y = -this.height * message.resolution; + + // scale the image + this.scaleX = message.resolution; + this.scaleY = message.resolution; + this.width *= this.scaleX; + this.height *= this.scaleY; + + // set the pose + this.x += this.pose.position.x; + this.y -= this.pose.position.y; +}; +ROS2D.ImageMap.prototype.__proto__ = createjs.Bitmap.prototype; + +/** + * @author Russell Toris - rctoris@wpi.edu + */ + +/** + * A image map is a PNG image scaled to fit to the dimensions of a OccupancyGrid. + * + * Emits the following events: + * * 'change' - there was an update or change in the map + * + * @constructor + * @param options - object with following keys: + * * ros - the ROSLIB.Ros connection handle + * * topic (optional) - the map meta data topic to listen to + * * image - the image URL to load + * * rootObject (optional) - the root object to add this marker to + */ +ROS2D.ImageMapClient = function(options) { + var that = this; + options = options || {}; + var ros = options.ros; + var topic = options.topic || '/map_metadata'; + this.image = options.image; + this.rootObject = options.rootObject || new createjs.Container(); + + // create an empty shape to start with + this.currentImage = new createjs.Shape(); + + // subscribe to the topic + var rosTopic = new ROSLIB.Topic({ + ros : ros, + name : topic, + messageType : 'nav_msgs/MapMetaData' + }); + + rosTopic.subscribe(function(message) { + // we only need this once + rosTopic.unsubscribe(); + + // create the image + that.currentImage = new ROS2D.ImageMap({ + message : message, + image : that.image + }); + that.rootObject.addChild(that.currentImage); + // work-around for a bug in easeljs -- needs a second object to render correctly + that.rootObject.addChild(new ROS2D.Grid({size:1})); + + that.emit('change'); + }); +}; +ROS2D.ImageMapClient.prototype.__proto__ = EventEmitter2.prototype; + +/** + * @author Russell Toris - rctoris@wpi.edu + */ + +/** + * An OccupancyGrid can convert a ROS occupancy grid message into a createjs Bitmap object. + * + * @constructor + * @param options - object with following keys: + * * message - the occupancy grid message + */ +ROS2D.OccupancyGrid = function(options) { + options = options || {}; + var message = options.message; + + // internal drawing canvas + var canvas = document.createElement('canvas'); + var context = canvas.getContext('2d'); + + // save the metadata we need + this.pose = new ROSLIB.Pose({ + position : message.info.origin.position, + orientation : message.info.origin.orientation + }); + + // set the size + this.width = message.info.width; + this.height = message.info.height; + canvas.width = this.width; + canvas.height = this.height; + + var imageData = context.createImageData(this.width, this.height); + for ( var row = 0; row < this.height; row++) { + for ( var col = 0; col < this.width; col++) { + // determine the index into the map data + var mapI = col + ((this.height - row - 1) * this.width); + // determine the value + var data = message.data[mapI]; + var val; + if (data === 100) { + val = 0; + } else if (data === 0) { + val = 255; + } else { + val = 127; + } + + // determine the index into the image data array + var i = (col + (row * this.width)) * 4; + // r + imageData.data[i] = val; + // g + imageData.data[++i] = val; + // b + imageData.data[++i] = val; + // a + imageData.data[++i] = 255; + } + } + context.putImageData(imageData, 0, 0); + + // create the bitmap + createjs.Bitmap.call(this, canvas); + // change Y direction + this.y = -this.height * message.info.resolution; + + // scale the image + this.scaleX = message.info.resolution; + this.scaleY = message.info.resolution; + this.width *= this.scaleX; + this.height *= this.scaleY; + + // set the pose + this.x += this.pose.position.x; + this.y -= this.pose.position.y; +}; +ROS2D.OccupancyGrid.prototype.__proto__ = createjs.Bitmap.prototype; + +/** + * @author Russell Toris - rctoris@wpi.edu + */ + +/** + * A map that listens to a given occupancy grid topic. + * + * Emits the following events: + * * 'change' - there was an update or change in the map + * + * @constructor + * @param options - object with following keys: + * * ros - the ROSLIB.Ros connection handle + * * topic (optional) - the map topic to listen to + * * rootObject (optional) - the root object to add this marker to + * * continuous (optional) - if the map should be continuously loaded (e.g., for SLAM) + */ +ROS2D.OccupancyGridClient = function(options) { + var that = this; + options = options || {}; + var ros = options.ros; + var topic = options.topic || '/map'; + this.continuous = options.continuous; + this.rootObject = options.rootObject || new createjs.Container(); + + // current grid that is displayed + // create an empty shape to start with, so that the order remains correct. + this.currentGrid = new createjs.Shape(); + this.rootObject.addChild(this.currentGrid); + // work-around for a bug in easeljs -- needs a second object to render correctly + this.rootObject.addChild(new ROS2D.Grid({size:1})); + + // subscribe to the topic + var rosTopic = new ROSLIB.Topic({ + ros : ros, + name : topic, + messageType : 'nav_msgs/OccupancyGrid', + compression : 'png' + }); + + rosTopic.subscribe(function(message) { + // check for an old map + var index = null; + if (that.currentGrid) { + index = that.rootObject.getChildIndex(that.currentGrid); + that.rootObject.removeChild(that.currentGrid); + } + + that.currentGrid = new ROS2D.OccupancyGrid({ + message : message + }); + if (index !== null) { + that.rootObject.addChildAt(that.currentGrid, index); + } + else { + that.rootObject.addChild(that.currentGrid); + } + + that.emit('change'); + + // check if we should unsubscribe + if (!that.continuous) { + rosTopic.unsubscribe(); + } + }); +}; +ROS2D.OccupancyGridClient.prototype.__proto__ = EventEmitter2.prototype; + +/** + * @author Jihoon Lee- jihoonlee.in@gmail.com + * @author Russell Toris - rctoris@wpi.edu + */ + +/** + * A static map that receives from map_server. + * + * Emits the following events: + * * 'change' - there was an update or change in the map + * + * @constructor + * @param options - object with following keys: + * * ros - the ROSLIB.Ros connection handle + * * service (optional) - the map topic to listen to, like '/static_map' + * * rootObject (optional) - the root object to add this marker to + */ +ROS2D.OccupancyGridSrvClient = function(options) { + var that = this; + options = options || {}; + var ros = options.ros; + var service = options.service || '/static_map'; + this.rootObject = options.rootObject || new createjs.Container(); + + // current grid that is displayed + this.currentGrid = null; + + // Setting up to the service + var rosService = new ROSLIB.Service({ + ros : ros, + name : service, + serviceType : 'nav_msgs/GetMap', + compression : 'png' + }); + + rosService.callService(new ROSLIB.ServiceRequest(),function(response) { + // check for an old map + if (that.currentGrid) { + that.rootObject.removeChild(that.currentGrid); + } + + that.currentGrid = new ROS2D.OccupancyGrid({ + message : response.map + }); + that.rootObject.addChild(that.currentGrid); + + that.emit('change', that.currentGrid); + }); +}; +ROS2D.OccupancyGridSrvClient.prototype.__proto__ = EventEmitter2.prototype; + +/** + * @author Bart van Vliet - bart@dobots.nl + */ + +/** + * An arrow with line and triangular head, based on the navigation arrow. + * Aims to the left at 0 rotation, as would be expected. + * + * @constructor + * @param options - object with following keys: + * * size (optional) - the size of the marker + * * strokeSize (optional) - the size of the outline + * * strokeColor (optional) - the createjs color for the stroke + * * fillColor (optional) - the createjs color for the fill + * * pulse (optional) - if the marker should "pulse" over time + */ +ROS2D.ArrowShape = function(options) { + var that = this; + options = options || {}; + var size = options.size || 10; + var strokeSize = options.strokeSize || 3; + var strokeColor = options.strokeColor || createjs.Graphics.getRGB(0, 0, 0); + var fillColor = options.fillColor || createjs.Graphics.getRGB(255, 0, 0); + var pulse = options.pulse; + + // draw the arrow + var graphics = new createjs.Graphics(); + + var headLen = size / 3.0; + var headWidth = headLen * 2.0 / 3.0; + + graphics.setStrokeStyle(strokeSize); + graphics.beginStroke(strokeColor); + graphics.moveTo(0, 0); + graphics.lineTo(size-headLen, 0); + + graphics.beginFill(fillColor); + graphics.moveTo(size, 0); + graphics.lineTo(size-headLen, headWidth / 2.0); + graphics.lineTo(size-headLen, -headWidth / 2.0); + graphics.closePath(); + graphics.endFill(); + graphics.endStroke(); + + // create the shape + createjs.Shape.call(this, graphics); + + // check if we are pulsing + if (pulse) { + // have the model "pulse" + var growCount = 0; + var growing = true; + createjs.Ticker.addEventListener('tick', function() { + if (growing) { + that.scaleX *= 1.035; + that.scaleY *= 1.035; + growing = (++growCount < 10); + } else { + that.scaleX /= 1.035; + that.scaleY /= 1.035; + growing = (--growCount < 0); + } + }); + } +}; +ROS2D.ArrowShape.prototype.__proto__ = createjs.Shape.prototype; + +/** + * @author Raffaello Bonghi - raffaello.bonghi@officinerobotiche.it + */ + +/** + * A Grid object draw in map. + * + * @constructor + * @param options - object with following keys: + * * size (optional) - the size of the grid + * * cellSize (optional) - the cell size of map + * * lineWidth (optional) - the width of the lines in the grid + */ + ROS2D.Grid = function(options) { + var that = this; + options = options || {}; + var size = options.size || 10; + var cellSize = options.cellSize || 0.1; + var lineWidth = options.lineWidth || 0.001; + // draw the arrow + var graphics = new createjs.Graphics(); + // line width + graphics.setStrokeStyle(lineWidth*5); + graphics.beginStroke(createjs.Graphics.getRGB(0, 0, 0)); + graphics.beginFill(createjs.Graphics.getRGB(255, 0, 0)); + graphics.moveTo(-size*cellSize, 0); + graphics.lineTo(size*cellSize, 0); + graphics.moveTo(0, -size*cellSize); + graphics.lineTo(0, size*cellSize); + graphics.endFill(); + graphics.endStroke(); + + graphics.setStrokeStyle(lineWidth); + graphics.beginStroke(createjs.Graphics.getRGB(0, 0, 0)); + graphics.beginFill(createjs.Graphics.getRGB(255, 0, 0)); + for (var i = -size; i <= size; i++) { + graphics.moveTo(-size*cellSize, i * cellSize); + graphics.lineTo(size*cellSize, i * cellSize); + graphics.moveTo(i * cellSize, -size*cellSize); + graphics.lineTo(i * cellSize, size*cellSize); + } + graphics.endFill(); + graphics.endStroke(); + // create the shape + createjs.Shape.call(this, graphics); + +}; +ROS2D.Grid.prototype.__proto__ = createjs.Shape.prototype; + +/** + * @author Russell Toris - rctoris@wpi.edu + */ + +/** + * A navigation arrow is a directed triangle that can be used to display orientation. + * + * @constructor + * @param options - object with following keys: + * * size (optional) - the size of the marker + * * strokeSize (optional) - the size of the outline + * * strokeColor (optional) - the createjs color for the stroke + * * fillColor (optional) - the createjs color for the fill + * * pulse (optional) - if the marker should "pulse" over time + */ +ROS2D.NavigationArrow = function(options) { + var that = this; + options = options || {}; + var size = options.size || 10; + var strokeSize = options.strokeSize || 3; + var strokeColor = options.strokeColor || createjs.Graphics.getRGB(0, 0, 0); + var fillColor = options.fillColor || createjs.Graphics.getRGB(255, 0, 0); + var pulse = options.pulse; + + // draw the arrow + var graphics = new createjs.Graphics(); + // line width + graphics.setStrokeStyle(strokeSize); + graphics.moveTo(-size / 2.0, -size / 2.0); + graphics.beginStroke(strokeColor); + graphics.beginFill(fillColor); + graphics.lineTo(size, 0); + graphics.lineTo(-size / 2.0, size / 2.0); + graphics.closePath(); + graphics.endFill(); + graphics.endStroke(); + + // create the shape + createjs.Shape.call(this, graphics); + + // check if we are pulsing + if (pulse) { + // have the model "pulse" + var growCount = 0; + var growing = true; + createjs.Ticker.addEventListener('tick', function() { + if (growing) { + that.scaleX *= 1.035; + that.scaleY *= 1.035; + growing = (++growCount < 10); + } else { + that.scaleX /= 1.035; + that.scaleY /= 1.035; + growing = (--growCount < 0); + } + }); + } +}; +ROS2D.NavigationArrow.prototype.__proto__ = createjs.Shape.prototype; + +/** + * @author Inigo Gonzalez - ingonza85@gmail.com + */ + +/** + * A navigation image that can be used to display orientation. + * + * @constructor + * @param options - object with following keys: + * * size (optional) - the size of the marker + * * image - the image to use as a marker + * * pulse (optional) - if the marker should "pulse" over time + */ +ROS2D.NavigationImage = function(options) { + var that = this; + options = options || {}; + var size = options.size || 10; + var image_url = options.image; + var pulse = options.pulse; + var alpha = options.alpha || 1; + + var originals = {}; + + var paintImage = function(){ + createjs.Bitmap.call(that, image); + var scale = calculateScale(size); + that.alpha = alpha; + that.scaleX = scale; + that.scaleY = scale; + that.regY = that.image.height/2; + that.regX = that.image.width/2; + originals['rotation'] = that.rotation; + Object.defineProperty( that, 'rotation', { + get: function(){ return originals['rotation'] + 90; }, + set: function(value){ originals['rotation'] = value; } + }); + if (pulse) { + // have the model "pulse" + var growCount = 0; + var growing = true; + var SCALE_SIZE = 1.020; + createjs.Ticker.addEventListener('tick', function() { + if (growing) { + that.scaleX *= SCALE_SIZE; + that.scaleY *= SCALE_SIZE; + growing = (++growCount < 10); + } else { + that.scaleX /= SCALE_SIZE; + that.scaleY /= SCALE_SIZE; + growing = (--growCount < 0); + } + }); + } + }; + + var calculateScale = function(_size){ + return _size / image.width; + }; + + var image = new Image(); + image.onload = paintImage; + image.src = image_url; + +}; + +ROS2D.NavigationImage.prototype.__proto__ = createjs.Bitmap.prototype; + +/** + * @author Bart van Vliet - bart@dobots.nl + */ + +/** + * A shape to draw a nav_msgs/Path msg + * + * @constructor + * @param options - object with following keys: + * * path (optional) - the initial path to draw + * * strokeSize (optional) - the size of the outline + * * strokeColor (optional) - the createjs color for the stroke + */ +ROS2D.PathShape = function(options) { + options = options || {}; + var path = options.path; + this.strokeSize = options.strokeSize || 3; + this.strokeColor = options.strokeColor || createjs.Graphics.getRGB(0, 0, 0); + + // draw the line + this.graphics = new createjs.Graphics(); + + if (path !== null && typeof path !== 'undefined') { + this.graphics.setStrokeStyle(this.strokeSize); + this.graphics.beginStroke(this.strokeColor); + this.graphics.moveTo(path.poses[0].pose.position.x / this.scaleX, path.poses[0].pose.position.y / -this.scaleY); + for (var i=1; i 1 point, 0 lines + // 1 point -> 2 points, lines: add line between previous and new point, add line between new point and first point + // 2 points -> 3 points, 3 lines: change last line, add line between new point and first point + // 3 points -> 4 points, 4 lines: change last line, add line between new point and first point + // etc + + if (numPoints < 2) { + // Now 1 point + } + else if (numPoints < 3) { + // Now 2 points: add line between previous and new point + var line = this.createLineShape(this.pointContainer.getChildAt(numPoints-2), point); + this.lineContainer.addChild(line); + } + if (numPoints > 2) { + // Now 3 or more points: change last line + this.editLineShape(this.lineContainer.getChildAt(numPoints-2), this.pointContainer.getChildAt(numPoints-2), point); + } + if (numPoints > 1) { + // Now 2 or more points: add line between new point and first point + var lineEnd = this.createLineShape(point, this.pointContainer.getChildAt(0)); + this.lineContainer.addChild(lineEnd); + } + + this.drawFill(); +}; + +/** + * Removes a point from the polygon + * + * @param obj either an index (integer) or a point shape of the polygon + */ +ROS2D.PolygonMarker.prototype.remPoint = function(obj) { + var index; +// var point; + if (obj instanceof createjs.Shape) { + index = this.pointContainer.getChildIndex(obj); +// point = obj; + } + else { + index = obj; +// point = this.pointContainer.getChildAt(index); + } + + // 0 points -> 0 points, 0 lines + // 1 point -> 0 points, 0 lines + // 2 points -> 1 point, 0 lines: remove all lines + // 3 points -> 2 points, 2 lines: change line before point to remove, remove line after point to remove + // 4 points -> 3 points, 3 lines: change line before point to remove, remove line after point to remove + // etc + + var numPoints = this.pointContainer.getNumChildren(); + + if (numPoints < 2) { + + } + else if (numPoints < 3) { + // 2 points: remove all lines + this.lineContainer.removeAllChildren(); + } + else { + // 3 or more points: change line before point to remove, remove line after point to remove + this.editLineShape( + this.lineContainer.getChildAt((index-1+numPoints)%numPoints), + this.pointContainer.getChildAt((index-1+numPoints)%numPoints), + this.pointContainer.getChildAt((index+1)%numPoints) + ); + this.lineContainer.removeChildAt(index); + } + this.pointContainer.removeChildAt(index); +// this.points.splice(index, 1); + + this.drawFill(); +}; + +/** + * Moves a point of the polygon + * + * @param obj either an index (integer) or a point shape of the polygon + * @param position of type ROSLIB.Vector3 + */ +ROS2D.PolygonMarker.prototype.movePoint = function(obj, newPos) { + var index; + var point; + if (obj instanceof createjs.Shape) { + index = this.pointContainer.getChildIndex(obj); + point = obj; + } + else { + index = obj; + point = this.pointContainer.getChildAt(index); + } + point.x = newPos.x; + point.y = -newPos.y; + + var numPoints = this.pointContainer.getNumChildren(); + if (numPoints > 1) { + // line before moved point + var line1 = this.lineContainer.getChildAt((index-1+numPoints)%numPoints); + this.editLineShape(line1, this.pointContainer.getChildAt((index-1+numPoints)%numPoints), point); + + // line after moved point + var line2 = this.lineContainer.getChildAt(index); + this.editLineShape(line2, point, this.pointContainer.getChildAt((index+1)%numPoints)); + } + + this.drawFill(); +}; + +/** + * Splits a line of the polygon: inserts a point at the center of the line + * + * @param obj either an index (integer) or a line shape of the polygon + */ +ROS2D.PolygonMarker.prototype.splitLine = function(obj) { + var index; + var line; + if (obj instanceof createjs.Shape) { + index = this.lineContainer.getChildIndex(obj); + line = obj; + } + else { + index = obj; + line = this.lineContainer.getChildAt(index); + } + var numPoints = this.pointContainer.getNumChildren(); + var xs = this.pointContainer.getChildAt(index).x; + var ys = this.pointContainer.getChildAt(index).y; + var xe = this.pointContainer.getChildAt((index+1)%numPoints).x; + var ye = this.pointContainer.getChildAt((index+1)%numPoints).y; + var xh = (xs+xe)/2.0; + var yh = (ys+ye)/2.0; + var pos = new ROSLIB.Vector3({ x:xh, y:-yh }); + + // Add a point in the center of the line to split + var point = this.createPointShape(pos); + this.pointContainer.addChildAt(point, index+1); + ++numPoints; + + // Add a line between the new point and the end of the line to split + var lineNew = this.createLineShape(point, this.pointContainer.getChildAt((index+2)%numPoints)); + this.lineContainer.addChildAt(lineNew, index+1); + + // Set the endpoint of the line to split to the new point + this.editLineShape(line, this.pointContainer.getChildAt(index), point); + + this.drawFill(); +}; + +/** + * Internal use only + */ +ROS2D.PolygonMarker.prototype.drawFill = function() { + var numPoints = this.pointContainer.getNumChildren(); + if (numPoints > 2) { + var g = this.fillShape.graphics; + g.clear(); + g.setStrokeStyle(0); + g.moveTo(this.pointContainer.getChildAt(0).x, this.pointContainer.getChildAt(0).y); + g.beginStroke(); + g.beginFill(this.fillColor); + for (var i=1; i this.minDist) { + this.graphics.lineTo(pose.position.x / this.scaleX, pose.position.y / -this.scaleY); + this.poses.push(pose); + } + } + if (this.maxPoses > 0 && this.maxPoses < this.poses.length) { + this.popFront(); + } +}; + +/** + * Removes front pose and updates the graphics + */ +ROS2D.TraceShape.prototype.popFront = function() { + if (this.poses.length > 0) { + this.poses.shift(); + // TODO: shift drawing instructions rather than doing it all over + this.graphics.clear(); + this.graphics.setStrokeStyle(this.strokeSize); + this.graphics.beginStroke(this.strokeColor); + this.graphics.lineTo(this.poses[0].position.x / this.scaleX, this.poses[0].position.y / -this.scaleY); + for (var i=1; i