You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

336 lines
10 KiB

/**
* 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: 'white',
'stroke-opacity': 0.2, // the border
},
blocked: {
fill: 'grey',
'stroke-opacity': 0.2,
},
start: {
fill: '#0d0',
'stroke-opacity': 0.2,
},
end: {
fill: '#e40',
'stroke-opacity': 0.2,
},
opened: {
fill: '#98fb98',
'stroke-opacity': 0.2,
},
closed: {
fill: '#afeeee',
'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;
},
};