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.

508 lines
15 KiB

/**
* The visualization controller will works as a state machine.
* See files under the `doc` folder for transition descriptions.
* See https://github.com/jakesgordon/javascript-state-machine
* for the document of the StateMachine module.
*/
var Controller = StateMachine.create({
initial: 'none',
events: [
{
name: 'init',
from: 'none',
to: 'ready'
},
{
name: 'search',
from: 'starting',
to: 'searching'
},
{
name: 'pause',
from: 'searching',
to: 'paused'
},
{
name: 'finish',
from: 'searching',
to: 'finished'
},
{
name: 'resume',
from: 'paused',
to: 'searching'
},
{
name: 'cancel',
from: 'paused',
to: 'ready'
},
{
name: 'modify',
from: 'finished',
to: 'modified'
},
{
name: 'reset',
from: '*',
to: 'ready'
},
{
name: 'clear',
from: ['finished', 'modified'],
to: 'ready'
},
{
name: 'start',
from: ['ready', 'modified', 'restarting'],
to: 'starting'
},
{
name: 'restart',
from: ['searching', 'finished'],
to: 'restarting'
},
{
name: 'dragStart',
from: ['ready', 'finished'],
to: 'draggingStart'
},
{
name: 'dragEnd',
from: ['ready', 'finished'],
to: 'draggingEnd'
},
{
name: 'drawWall',
from: ['ready', 'finished'],
to: 'drawingWall'
},
{
name: 'eraseWall',
from: ['ready', 'finished'],
to: 'erasingWall'
},
{
name: 'rest',
from: ['draggingStart', 'draggingEnd', 'drawingWall', 'erasingWall'],
to : 'ready'
},
],
});
$.extend(Controller, {
gridSize: [64, 36], // number of nodes horizontally and vertically
operationsPerSecond: 300,
/**
* Asynchronous transition from `none` state to `ready` state.
*/
onleavenone: function() {
var numCols = this.gridSize[0],
numRows = this.gridSize[1];
this.grid = new PF.Grid(numCols, numRows);
View.init({
numCols: numCols,
numRows: numRows
});
View.generateGrid(function() {
Controller.setDefaultStartEndPos();
Controller.bindEvents();
Controller.transition(); // transit to the next state (ready)
});
this.$buttons = $('.control_button');
this.hookPathFinding();
return StateMachine.ASYNC;
// => ready
},
ondrawWall: function(event, from, to, gridX, gridY) {
this.setWalkableAt(gridX, gridY, false);
// => drawingWall
},
oneraseWall: function(event, from, to, gridX, gridY) {
this.setWalkableAt(gridX, gridY, true);
// => erasingWall
},
onsearch: function(event, from, to) {
var grid,
timeStart, timeEnd,
finder = Panel.getFinder();
timeStart = window.performance ? performance.now() : Date.now();
grid = this.grid.clone();
this.path = finder.findPath(
this.startX, this.startY, this.endX, this.endY, grid
);
this.operationCount = this.operations.length;
timeEnd = window.performance ? performance.now() : Date.now();
this.timeSpent = (timeEnd - timeStart).toFixed(4);
this.loop();
// => searching
},
onrestart: function() {
// When clearing the colorized nodes, there may be
// nodes still animating, which is an asynchronous procedure.
// Therefore, we have to defer the `abort` routine to make sure
// that all the animations are done by the time we clear the colors.
// The same reason applies for the `onreset` event handler.
setTimeout(function() {
Controller.clearOperations();
Controller.clearFootprints();
Controller.start();
}, View.nodeColorizeEffect.duration * 1.2);
// => restarting
},
onpause: function(event, from, to) {
// => paused
},
onresume: function(event, from, to) {
this.loop();
// => searching
},
oncancel: function(event, from, to) {
this.clearOperations();
this.clearFootprints();
// => ready
},
onfinish: function(event, from, to) {
View.showStats({
pathLength: PF.Util.pathLength(this.path),
timeSpent: this.timeSpent,
operationCount: this.operationCount,
});
View.drawPath(this.path);
// => finished
},
onclear: function(event, from, to) {
this.clearOperations();
this.clearFootprints();
// => ready
},
onmodify: function(event, from, to) {
// => modified
},
onreset: function(event, from, to) {
setTimeout(function() {
Controller.clearOperations();
Controller.clearAll();
Controller.buildNewGrid();
}, View.nodeColorizeEffect.duration * 1.2);
// => ready
},
/**
* The following functions are called on entering states.
*/
onready: function() {
console.log('=> ready');
this.setButtonStates({
id: 1,
text: 'Start Search',
enabled: true,
callback: $.proxy(this.start, this),
}, {
id: 2,
text: 'Pause Search',
enabled: false,
}, {
id: 3,
text: 'Clear Walls',
enabled: true,
callback: $.proxy(this.reset, this),
});
// => [starting, draggingStart, draggingEnd, drawingStart, drawingEnd]
},
onstarting: function(event, from, to) {
console.log('=> starting');
// Clears any existing search progress
this.clearFootprints();
this.setButtonStates({
id: 2,
enabled: true,
});
this.search();
// => searching
},
onsearching: function() {
console.log('=> searching');
this.setButtonStates({
id: 1,
text: 'Restart Search',
enabled: true,
callback: $.proxy(this.restart, this),
}, {
id: 2,
text: 'Pause Search',
enabled: true,
callback: $.proxy(this.pause, this),
});
// => [paused, finished]
},
onpaused: function() {
console.log('=> paused');
this.setButtonStates({
id: 1,
text: 'Resume Search',
enabled: true,
callback: $.proxy(this.resume, this),
}, {
id: 2,
text: 'Cancel Search',
enabled: true,
callback: $.proxy(this.cancel, this),
});
// => [searching, ready]
},
onfinished: function() {
console.log('=> finished');
this.setButtonStates({
id: 1,
text: 'Restart Search',
enabled: true,
callback: $.proxy(this.restart, this),
}, {
id: 2,
text: 'Clear Path',
enabled: true,
callback: $.proxy(this.clear, this),
});
},
onmodified: function() {
console.log('=> modified');
this.setButtonStates({
id: 1,
text: 'Start Search',
enabled: true,
callback: $.proxy(this.start, this),
}, {
id: 2,
text: 'Clear Path',
enabled: true,
callback: $.proxy(this.clear, this),
});
},
/**
* Define setters and getters of PF.Node, then we can get the operations
* of the pathfinding.
*/
hookPathFinding: function() {
PF.Node.prototype = {
get opened() {
return this._opened;
},
set opened(v) {
this._opened = v;
Controller.operations.push({
x: this.x,
y: this.y,
attr: 'opened',
value: v
});
},
get closed() {
return this._closed;
},
set closed(v) {
this._closed = v;
Controller.operations.push({
x: this.x,
y: this.y,
attr: 'closed',
value: v
});
},
get tested() {
return this._tested;
},
set tested(v) {
this._tested = v;
Controller.operations.push({
x: this.x,
y: this.y,
attr: 'tested',
value: v
});
},
};
this.operations = [];
},
bindEvents: function() {
$('#draw_area').mousedown($.proxy(this.mousedown, this));
$(window)
.mousemove($.proxy(this.mousemove, this))
.mouseup($.proxy(this.mouseup, this));
},
loop: function() {
var interval = 1000 / this.operationsPerSecond;
(function loop() {
if (!Controller.is('searching')) {
return;
}
Controller.step();
setTimeout(loop, interval);
})();
},
step: function() {
var operations = this.operations,
op, isSupported;
do {
if (!operations.length) {
this.finish(); // transit to `finished` state
return;
}
op = operations.shift();
isSupported = View.supportedOperations.indexOf(op.attr) !== -1;
} while (!isSupported);
View.setAttributeAt(op.x, op.y, op.attr, op.value);
},
clearOperations: function() {
this.operations = [];
},
clearFootprints: function() {
View.clearFootprints();
View.clearPath();
},
clearAll: function() {
this.clearFootprints();
View.clearBlockedNodes();
},
buildNewGrid: function() {
this.grid = new PF.Grid(this.gridSize[0], this.gridSize[1]);
},
mousedown: function (event) {
var coord = View.toGridCoordinate(event.pageX, event.pageY),
gridX = coord[0],
gridY = coord[1],
grid = this.grid;
if (this.can('dragStart') && this.isStartPos(gridX, gridY)) {
this.dragStart();
return;
}
if (this.can('dragEnd') && this.isEndPos(gridX, gridY)) {
this.dragEnd();
return;
}
if (this.can('drawWall') && grid.isWalkableAt(gridX, gridY)) {
this.drawWall(gridX, gridY);
return;
}
if (this.can('eraseWall') && !grid.isWalkableAt(gridX, gridY)) {
this.eraseWall(gridX, gridY);
}
},
mousemove: function(event) {
var coord = View.toGridCoordinate(event.pageX, event.pageY),
grid = this.grid,
gridX = coord[0],
gridY = coord[1];
if (this.isStartOrEndPos(gridX, gridY)) {
return;
}
switch (this.current) {
case 'draggingStart':
if (grid.isWalkableAt(gridX, gridY)) {
this.setStartPos(gridX, gridY);
}
break;
case 'draggingEnd':
if (grid.isWalkableAt(gridX, gridY)) {
this.setEndPos(gridX, gridY);
}
break;
case 'drawingWall':
this.setWalkableAt(gridX, gridY, false);
break;
case 'erasingWall':
this.setWalkableAt(gridX, gridY, true);
break;
}
},
mouseup: function(event) {
if (Controller.can('rest')) {
Controller.rest();
}
},
setButtonStates: function() {
$.each(arguments, function(i, opt) {
var $button = Controller.$buttons.eq(opt.id - 1);
if (opt.text) {
$button.text(opt.text);
}
if (opt.callback) {
$button
.unbind('click')
.click(opt.callback);
}
if (opt.enabled === undefined) {
return;
} else if (opt.enabled) {
$button.removeAttr('disabled');
} else {
$button.attr({ disabled: 'disabled' });
}
});
},
/**
* When initializing, this method will be called to set the positions
* of start node and end node.
* It will detect user's display size, and compute the best positions.
*/
setDefaultStartEndPos: function() {
var width, height,
marginRight, availWidth,
centerX, centerY,
endX, endY,
nodeSize = View.nodeSize;
width = $(window).width();
height = $(window).height();
marginRight = $('#algorithm_panel').width();
availWidth = width - marginRight;
centerX = Math.ceil(availWidth / 2 / nodeSize);
centerY = Math.floor(height / 2 / nodeSize);
this.setStartPos(centerX - 5, centerY);
this.setEndPos(centerX + 5, centerY);
},
setStartPos: function(gridX, gridY) {
this.startX = gridX;
this.startY = gridY;
View.setStartPos(gridX, gridY);
},
setEndPos: function(gridX, gridY) {
this.endX = gridX;
this.endY = gridY;
View.setEndPos(gridX, gridY);
},
setWalkableAt: function(gridX, gridY, walkable) {
this.grid.setWalkableAt(gridX, gridY, walkable);
View.setAttributeAt(gridX, gridY, 'walkable', walkable);
},
isStartPos: function(gridX, gridY) {
return gridX === this.startX && gridY === this.startY;
},
isEndPos: function(gridX, gridY) {
return gridX === this.endX && gridY === this.endY;
},
isStartOrEndPos: function(gridX, gridY) {
return this.isStartPos(gridX, gridY) || this.isEndPos(gridX, gridY);
},
});