text: /**
   * The pathfinding visualization.
   * It uses raphael.js to show the grids.
   */
  var View = {
      nodeSize: 30, // width and height of a single node, in pixel
      nodeStyle: {
          normal: {
              fill:  'rgb(178, 91, 250)',
              'stroke-opacity': 0.4, // the border
          },
          blocked: {
              fill: '#ddd',
              'stroke-opacity': 0.2,
          },
          start: {
              fill: 'rgb(182, 5, 5)',
              'stroke-opacity': 0.2,
          },
          end: {
              fill: ' whitesmoke',
              'stroke-opacity': 0.2,
          },
          opened: {
              fill: 'orange',
              'stroke-opacity': 0.2,
          },
          closed: {
              fill: '#333',
              'stroke-opacity': 0.2,
          },
          failed: {
              fill: '#ff8888',
              'stroke-opacity': 0.2,
          },
          tested: {
              fill: '#e5e5e5',
              'stroke-opacity': 0.2,
          },
      },
      nodeColorizeEffect: {
          duration: 50,
      },
      nodeZoomEffect: {
          duration: 200,
          transform: 's1.2', // scale by 1.2x
          transformBack: 's1.0',
      },
      pathStyle: {
          stroke: 'yellow',
          'stroke-width': 3,
      },
      supportedOperations: ['opened', 'closed', 'tested'],
      init: function(opts) {
          this.numCols      = opts.numCols;
          this.numRows      = opts.numRows;
          this.paper        = Raphael('draw_area');
          this.$stats       = $('#stats');
      },
      /**
       * Generate the grid asynchronously.
       * This method will be a very expensive task.
       * Therefore, in order to not to block the rendering of browser ui,
       * I decomposed the task into smaller ones. Each will only generate a row.
       */
      generateGrid: function(callback) {
          var i, j, x, y,
              rect,
              normalStyle, nodeSize,
              createRowTask, sleep, tasks,
              nodeSize    = this.nodeSize,
              normalStyle = this.nodeStyle.normal,
              numCols     = this.numCols,
              numRows     = this.numRows,
              paper       = this.paper,
              rects       = this.rects = [],
              $stats      = this.$stats;
  
          paper.setSize(numCols * nodeSize, numRows * nodeSize);
  
          createRowTask = function(rowId) {
              return function(done) {
                  rects[rowId] = [];
                  for (j = 0; j < numCols; ++j) {
                      x = j * nodeSize;
                      y = rowId * nodeSize;
  
                      rect = paper.rect(x, y, nodeSize, nodeSize);
                      rect.attr(normalStyle);
                      rects[rowId].push(rect);
                  }
                  $stats.text(
                      'generating grid ' +
                      Math.round((rowId + 1) / numRows * 100) + '%'
                  );
                  done(null);
              };
          };
  
          sleep = function(done) {
              setTimeout(function() {
                  done(null);
              }, 0);
          };
  
          tasks = [];
          for (i = 0; i < numRows; ++i) {
              tasks.push(createRowTask(i));
              tasks.push(sleep);
          }
  
          async.series(tasks, function() {
              if (callback) {
                  callback();
              }
          });
      },
      setStartPos: function(gridX, gridY) {
          var coord = this.toPageCoordinate(gridX, gridY);
          if (!this.startNode) {
              this.startNode = this.paper.rect(
                  coord[0],
                  coord[1],
                  this.nodeSize,
                  this.nodeSize
              ).attr(this.nodeStyle.normal)
               .animate(this.nodeStyle.start, 1000);
          } else {
              this.startNode.attr({ x: coord[0], y: coord[1] }).toFront();
          }
      },
      setEndPos: function(gridX, gridY) {
          var coord = this.toPageCoordinate(gridX, gridY);
          if (!this.endNode) {
              this.endNode = this.paper.rect(
                  coord[0],
                  coord[1],
                  this.nodeSize,
                  this.nodeSize
              ).attr(this.nodeStyle.normal)
               .animate(this.nodeStyle.end, 1000);
          } else {
              this.endNode.attr({ x: coord[0], y: coord[1] }).toFront();
          }
      },
      /**
       * Set the attribute of the node at the given coordinate.
       */
      setAttributeAt: function(gridX, gridY, attr, value) {
          var color, nodeStyle = this.nodeStyle;
          switch (attr) {
          case 'walkable':
              color = value ? nodeStyle.normal.fill : nodeStyle.blocked.fill;
              this.setWalkableAt(gridX, gridY, value);
              break;
          case 'opened':
              this.colorizeNode(this.rects[gridY][gridX], nodeStyle.opened.fill);
              this.setCoordDirty(gridX, gridY, true);
              break;
          case 'closed':
              this.colorizeNode(this.rects[gridY][gridX], nodeStyle.closed.fill);
              this.setCoordDirty(gridX, gridY, true);
              break;
          case 'tested':
              color = (value === true) ? nodeStyle.tested.fill : nodeStyle.normal.fill;
  
              this.colorizeNode(this.rects[gridY][gridX], color);
              this.setCoordDirty(gridX, gridY, true);
              break;
          case 'parent':
              // XXX: Maybe draw a line from this node to its parent?
              // This would be expensive.
              break;
          default:
              console.error('unsupported operation: ' + attr + ':' + value);
              return;
          }
      },
      colorizeNode: function(node, color) {
          node.animate({
              fill: color
          }, this.nodeColorizeEffect.duration);
      },
      zoomNode: function(node) {
          node.toFront().attr({
              transform: this.nodeZoomEffect.transform,
          }).animate({
              transform: this.nodeZoomEffect.transformBack,
          }, this.nodeZoomEffect.duration);
      },
      setWalkableAt: function(gridX, gridY, value) {
          var node, i, blockedNodes = this.blockedNodes;
          if (!blockedNodes) {
              blockedNodes = this.blockedNodes = new Array(this.numRows);
              for (i = 0; i < this.numRows; ++i) {
                  blockedNodes[i] = [];
              }
          }
          node = blockedNodes[gridY][gridX];
          if (value) {
              // clear blocked node
              if (node) {
                  this.colorizeNode(node, this.rects[gridY][gridX].attr('fill'));
                  this.zoomNode(node);
                  setTimeout(function() {
                      node.remove();
                  }, this.nodeZoomEffect.duration);
                  blockedNodes[gridY][gridX] = null;
              }
          } else {
              // draw blocked node
              if (node) {
                  return;
              }
              node = blockedNodes[gridY][gridX] = this.rects[gridY][gridX].clone();
              this.colorizeNode(node, this.nodeStyle.blocked.fill);
              this.zoomNode(node);
          }
      },
      clearFootprints: function() {
          var i, x, y, coord, coords = this.getDirtyCoords();
          for (i = 0; i < coords.length; ++i) {
              coord = coords[i];
              x = coord[0];
              y = coord[1];
              this.rects[y][x].attr(this.nodeStyle.normal);
              this.setCoordDirty(x, y, false);
          }
      },
      clearBlockedNodes: function() {
          var i, j, blockedNodes = this.blockedNodes;
          if (!blockedNodes) {
              return;
          }
          for (i = 0; i < this.numRows; ++i) {
              for (j = 0 ;j < this.numCols; ++j) {
                  if (blockedNodes[i][j]) {
                      blockedNodes[i][j].remove();
                      blockedNodes[i][j] = null;
                  }
              }
          }
      },
      drawPath: function(path) {
          if (!path.length) {
              return;
          }
          var svgPath = this.buildSvgPath(path);
          this.path = this.paper.path(svgPath).attr(this.pathStyle);
      },
      /**
       * Given a path, build its SVG represention.
       */
      buildSvgPath: function(path) {
          var i, strs = [], size = this.nodeSize;
  
          strs.push('M' + (path[0][0] * size + size / 2) + ' ' +
                    (path[0][1] * size + size / 2));
          for (i = 1; i < path.length; ++i) {
              strs.push('L' + (path[i][0] * size + size / 2) + ' ' +
                        (path[i][1] * size + size / 2));
          }
  
          return strs.join('');
      },
      clearPath: function() {
          if (this.path) {
              this.path.remove();
          }
      },
      /**
       * Helper function to convert the page coordinate to grid coordinate
       */
      toGridCoordinate: function(pageX, pageY) {
          return [
              Math.floor(pageX / this.nodeSize),
              Math.floor(pageY / this.nodeSize)
          ];
      },
      /**
       * helper function to convert the grid coordinate to page coordinate
       */
      toPageCoordinate: function(gridX, gridY) {
          return [
              gridX * this.nodeSize,
              gridY * this.nodeSize
          ];
      },
      showStats: function(opts) {
          var texts = [
              'length: ' + Math.round(opts.pathLength * 100) / 100,
              'time: ' + opts.timeSpent + 'ms',
              'operations: ' + opts.operationCount
          ];
          $('#stats').show().html(texts.join('<br>'));
      },
      setCoordDirty: function(gridX, gridY, isDirty) {
          var x, y,
              numRows = this.numRows,
              numCols = this.numCols,
              coordDirty;
  
          if (this.coordDirty === undefined) {
              coordDirty = this.coordDirty = [];
              for (y = 0; y < numRows; ++y) {
                  coordDirty.push([]);
                  for (x = 0; x < numCols; ++x) {
                      coordDirty[y].push(false);
                  }
              }
          }
  
          this.coordDirty[gridY][gridX] = isDirty;
      },
      getDirtyCoords: function() {
          var x, y,
              numRows = this.numRows,
              numCols = this.numCols,
              coordDirty = this.coordDirty,
              coords = [];
  
          if (coordDirty === undefined) {
              return [];
          }
  
          for (y = 0; y < numRows; ++y) {
              for (x = 0; x < numCols; ++x) {
                  if (coordDirty[y][x]) {
                      coords.push([x, y]);
                  }
              }
          }
          return coords;
      },
  };
  