<body>
  <div class="container">
    <main class="content">
      <div id="draw_area"></div>
      <div id="play_panel" class="">
        <button id="button1" class="control_button">Start</button>
        <button id="button2" class="control_button">Pause</button>
        <!--button id="button3" class="control_button">Clear Search</button-->
      </div>
  </main>
  <nav class="controls">

    <div id="algorithm_panel" class="panel right_panel">
      <header>
      <h3 class="header_title">Algorithm</h3>
      </header>
      <p>Select the search strategy and search options<p>

      <div class="accordion">
        <h3 id="astar_header"><a href="#">A*</a></h3>
        <div id="astar_section" class="finder_section">
          <header class="option_header">
            <h3>Heuristic</h3>
          </header>
          <div id="astar_heuristic" class="sub_options">
            <input type="radio" name="astar_heuristic" value="manhattan" checked />
            <label class="option_label">Manhattan</label> <br>
            <input type="radio" name="astar_heuristic" value="euclidean"/>
            <label class="option_label">Euclidean</label> <br>
            <input type="radio" name="astar_heuristic" value="octile"/>
            <label class="option_label">Octile</label> <br>
            <input type="radio" name="astar_heuristic" value="chebyshev"/>
            <label class="option_label">Chebyshev</label> <br>
          </div>

          <header class="option_header">
            <h3>Options</h3>
          </header>
          <div class="optional sub_options">
            <input type="checkbox" class="allow_diagonal" checked>
            <label class="option_label">Allow Diagonal</label> <br>
            <input type="checkbox" class="bi-directional">
            <label class="option_label">Bi-directional</label> <br>
            <input type="checkbox" class="dont_cross_corners">
            <label class="option_label">Don't Cross Corners</label> <br>
            <input class="spinner" name="astar_weight" value="1">
            <label class="option_label">Weight</label> <br>
          </div>
        </div>

        <h3 id="breadthfirst_header"><a href="#">Breadth-First-Search</a></h3>
        <div id="breadthfirst_section" class="finder_section">
          <header class="option_header">
            <h3>Options</h3>
          </header>
          <div class="optional sub_options">
            <input type="checkbox" class="allow_diagonal" checked>
            <label class="option_label">Allow Diagonal</label> <br>
            <input type="checkbox" class="bi-directional">
            <label class="option_label">Bi-directional</label> <br>
            <input type="checkbox" class="dont_cross_corners">
            <label class="option_label">Don't Cross Corners</label> <br>
          </div>
        </div>

        <h3 id="bestfirst_header"><a href="#">Best-First-Search</a></h3>
        <div id="bestfirst_section" class="finder_section">
          <header class="option_header">
            <h3>Heuristic</h3>
          </header>
          <div id="bestfirst_heuristic" class="sub_options">
            <input type="radio" name="bestfirst_heuristic" value="manhattan" checked />
            <label class="option_label">Manhattan</label> <br>
            <input type="radio" name="bestfirst_heuristic" value="euclidean"/>
            <label class="option_label">Euclidean</label> <br>
            <input type="radio" name="bestfirst_heuristic" value="octile"/>
            <label class="option_label">Octile</label> <br>
            <input type="radio" name="bestfirst_heuristic" value="chebyshev"/>
            <label class="option_label">Chebyshev</label> <br>
          </div>

          <header class="option_header">
            <h3>Options</h3>
          </header>
          <div class="optional sub_options">
            <input type="checkbox" class="allow_diagonal" checked>
            <label class="option_label">Allow Diagonal</label> <br>
            <input type="checkbox" class="bi-directional">
            <label class="option_label">Bi-directional</label> <br>
            <input type="checkbox" class="dont_cross_corners">
            <label class="option_label">Don't Cross Corners</label> <br>
          </div>
        </div>

        <h3 id="dijkstra_header"><a href="#">Dijkstra</a></h3>
        <div id="dijkstra_section" class="finder_section">
          <header class="option_header">
            <h3>Options</h3>
          </header>
          <div class="optional sub_options">
            <input type="checkbox" class="allow_diagonal" checked>
            <label class="option_label">Allow Diagonal</label> <br>
            <input type="checkbox" class="bi-directional">
            <label class="option_label">Bi-directional</label> <br>
            <input type="checkbox" class="dont_cross_corners">
            <label class="option_label">Don't Cross Corners</label> <br>
          </div>
        </div>

        <h3 id="jump_point_header"><a href="#">Jump Point Search</a></h3>
        <div id="jump_point_section" class="finder_section">
          <header class="option_header">
            <h3>Heuristic</h3>
          </header>
          <div id="jump_point_heuristic" class="sub_options">
            <input type="radio" name="jump_point_heuristic" value="manhattan" checked />
            <label class="option_label">Manhattan</label> <br>
            <input type="radio" name="jump_point_heuristic" value="euclidean"/>
            <label class="option_label">Euclidean</label> <br>
            <input type="radio" name="jump_point_heuristic" value="octile"/>
            <label class="option_label">Octile</label> <br>
            <input type="radio" name="jump_point_heuristic" value="chebyshev"/>
            <label class="option_label">Chebyshev</label> <br>
          </div>
          <header class="option_header">
            <h3>Options</h3>
          </header>
          <div class="optional sub_options">
            <input type="checkbox" class="track_recursion" checked>
            <label class="option_label">Visualize recursion</label> <br>
          </div>
        </div>

                <h3 id="ida_header"><a href="#">IDA*</a></h3>
        <div id="ida_section" class="finder_section">
          <header class="option_header">
            <h3>Heuristic</h3>
          </header>
          <div id="ida_heuristic" class="sub_options">
            <input type="radio" name="ida_heuristic" value="manhattan" checked />
            <label class="option_label">Manhattan</label> <br>
            <input type="radio" name="ida_heuristic" value="euclidean"/>
            <label class="option_label">Euclidean</label> <br>
            <input type="radio" name="ida_heuristic" value="octile"/>
            <label class="option_label">Octile</label> <br>
            <input type="radio" name="ida_heuristic" value="chebyshev"/>
            <label class="option_label">Chebyshev</label> <br>
          </div>
          <header class="option_header">
            <h3>Options</h3>
          </header>
          <div class="optional sub_options">
            <input type="checkbox" class="allow_diagonal" checked>
            <label class="option_label">Allow Diagonal</label> <br>
            <input type="checkbox" class="dont_cross_corners">
            <label class="option_label">Don't Cross Corners</label> <br>
            <input class="spinner" name="astar_weight" value="1">
            <label class="option_label">Weight</label> <br>
            <input class="spinner" name="time_limit" value="10">
            <label class="option_label">Seconds limit</label> <br>
            <input type="checkbox" class="track_recursion" checked />
            <label class="option_label">Visualize recursion</label> <br>
          </div>
        </div>

        <!-- Not working for some reason -->
        <!--h3 id="orth_jump_point_header"><a href="#">Orthogonal Jump Point Search</a></h3>
        <div id="orth_jump_point_section" class="finder_section">
          <header class="option_header">
            <h3>Heuristic</h3>
          </header>
          <div id="orth_jump_point_heuristic" class="sub_options">
            <input type="radio" name="orth_jump_point_heuristic" value="manhattan" checked />
            <label class="option_label">Manhattan</label> <br>
            <input type="radio" name="orth_jump_point_heuristic" value="euclidean"/>
            <label class="option_label">Euclidean</label> <br>
            <input type="radio" name="orth_jump_point_heuristic" value="octile"/>
            <label class="option_label">Octile</label> <br>
            <input type="radio" name="orth_jump_point_heuristic" value="chebyshev"/>
            <label class="option_label">Chebyshev</label> <br>
          </div>
          <header class="option_header">
            <h3>Options</h3>
          </header>
          <div class="optional sub_options">
            <input type="checkbox" class="track_recursion" checked>
            <label class="option_label">Visualize recursion</label> <br>
          </div>
        </div-->
      </div><!-- .accordion -->
    </div><!-- #algorithm_panel -->
  </nav>
  <aside class="help">
    <div id="instructions_panel" class="panel">
      <header>
        <h3 class="header_title">Help</h3>
        <!--span id="hide_instructions">hide</span-->
      </header>
      <div>
      <p>This is a visualization of the excellent <a href="https://github.com/qiao/PathFinding.js">Pathfinding.js</a> library. </p>
      <p>Draw a maze by clicking on the grid Then select a Search algorithm on the left and select start.</p>
      <h3 class="header_title">Stats</h3>
      <p>Results of the search:</p>
      <div id="stats" ></div>
      </div>
    </div>


  </aside>
  </div>
</body>
body {
    background: #454C51;
    padding: 0px;
    margin: 0px;
    overflow: hidden;
    font-family: Helvetica;
    cursor:crosshair;
}

input {
    cursor: pointer;
}

h1, h2, h3, h4, h5, h6 {
    margin: 0px;
    padding: 0px;
    font-weight: normal;
}

.panel {
    position: fixed;
    background-color: #383D41;
    color: #e0e0e0;
    border-radius: 2px;
    cursor: default;
}

#algorithm_panel {
    left: 0px;
    top: 0px;
    width: 20vw;
    right: 20px;
    height: 100%;
}

#instructions_panel {
    top: 0px;
    right: 0px;
    width: 20vw;
    height: 100%;
}

#algorithm_panel p, #instructions_panel p {
    padding-left: 1.2vw;
    padding-right: 1.2vw;
    font-size: 1.32vw;
    line-height: 1.6vw;
}

#instructions_panel a {
  color: #66A3EB !important;
}

#play_panel {
    background: 0px;
    position: absolute;
    top: 10px !important;
    left: 21vw;
    width:20vw;
}

#stats {
    color: #B8E986;
    margin:1vw;
    padding: 5px;
    background: #272A2D;
    font-family: "Lucida Console", Monaco, monospace;
    font-size: 1.1vw;
    line-height: 1.5vw;
    border-radius: 2px;
    width: 17vw;
}

#hide_instructions {
    position: absolute;
    right: 25px;
    top: 10px;
    font-size: 90%;
    cursor: pointer;
}

#hide_instruction:hover {
    color: #fff;
    text-decoration: underline;
}

.header_title {
    line-height: 1.8vw;
    font-weight: 400;
    color:#CDE8FF;
    padding-left: 1.2vw;
    position: relative;
    padding-top: 2vw;
}

h1, h2, h3, h4, h5, h6 {
    font-weight: normal;
    margin: 0;
    padding: 0;
}

.panel {
    color: #e0e0e0;
    cursor: default;
}

.option_header {
    font-size: 80%;
    margin-left: 20px;
}

.option_label {
    cursor: pointer;
}

button {
    border: none;
    background: rgba(255, 255, 255, 0.7);
    border-radius: 5px;
    cursor: pointer;
    font-size: 1.3vw;
    min-height: 40px;
    padding: 2px 8px;
    margin: 0 2%;
    width: auto;
}

button:hover {
    background: rgba(255, 255, 255, 0.9);
}

#button1 {
    background: #3490E7;
    margin:8px;
    box-shadow: inset 0px -3px 0px 0px #2E81CF;
    border-radius: 4px;
    color: #FFFFFF;
    line-height: 15px;
}

.finder_section {
    border: solid 1px #ddd;
    border-radius: 5px;
    margin: 5px;
}

.sub_options {
    color: #A0AFB7;
    padding: 2px;
    font-size: 14px;
    margin: 2px 12px 0;
}

a {
    color: #33f;
}

footer {
    color: #333;
    font-size: 70%;
    text-align: center;
    position: fixed;
    width: 200px;
    bottom: 10px;
    left: 50%;
    margin-left: -100px;
    background: rgba(255, 255, 255, 0.5);
    padding: 5px;
    border-radius: 5px;
}

.white {
    color: #fff;
}

.green {
    color: #0d0;
}

.red {
    color: #ff9166;
}

.spinner {
    width: 2em;
    margin: 5px;
}

/* Overrides */
.ui-accordion {
    margin: 1vw !important;
    width: 18vw !important;
}
.ui-accordion .ui-accordion-header {
    background: #444a50 !important;
    padding: 6px;
    font-size: 16px;
    border-radius: 0px;
    border-bottom: solid 1px #636A70 !important;
}

.ui-accordion-icons .ui-accordion-header a {
    padding-left: 1.2em !important;
}

.ui-accordion .ui-accordion-header {
    background: none repeat scroll 0 0 rgba(0, 0, 0, 0.5);
    border: 1px solid #333;
    cursor: pointer;
    margin-top: 1px;
    position: relative;
}

.ui-accordion .ui-accordion-header {
    background: none repeat scroll 0 0 #444a50 !important;
    border-bottom: 1px solid #636a70 !important;
    border-radius: 0;
    font-size: 16px;
    padding: 6px;
}

.ui-accordion .ui-accordion-content {
    border-radius: 0 0 2px 2px;
    background: #272A2D;
    border: none;
    display: none;
    padding: 1vw;
    margin: 0px;
    overflow: auto;
    padding: 8px;
    position: relative;
}
$(document).ready(function() {
    if (!Raphael.svg) {
        window.location = './notsupported.html';
    }

    // suppress select events
    $(window).bind('selectstart', function(event) {
        event.preventDefault();
    });

    // initialize visualization
    Panel.init();
    Controller.init();
});

/**
 * 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: [Math.round($(window).width() / 30)  + 1, Math.round($(window).height() / 30)  + 1], // 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();
            View.generateMaze();
            //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',
            enabled: true,
            callback: $.proxy(this.start, this),
        }, {
            id: 2,
            text: 'Pause',
            enabled: false,
        }, {
            id: 3,
            text: 'Clear',
            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',
            enabled: true,
            callback: $.proxy(this.restart, this),
        }, {
            id: 2,
            text: 'Pause',
            enabled: true,
            callback: $.proxy(this.pause, this),
        });
        // => [paused, finished]
    },
    onpaused: function() {
        console.log('=> paused');
        this.setButtonStates({
            id: 1,
            text: 'Resume',
            enabled: true,
            callback: $.proxy(this.resume, this),
        }, {
            id: 2,
            text: 'Cancel',
            enabled: true,
            callback: $.proxy(this.cancel, this),
        });
        // => [searching, ready]
    },
    onfinished: function() {
        console.log('=> finished');
        this.setButtonStates({
            id: 1,
            text: 'Restart',
            enabled: true,
            callback: $.proxy(this.restart, this),
        }, {
            id: 2,
            text: 'Clear',
            enabled: true,
            callback: $.proxy(this.clear, this),
        });
    },
    onmodified: function() {
        console.log('=> modified');
        this.setButtonStates({
            id: 1,
            text: 'Start',
            enabled: true,
            callback: $.proxy(this.start, this),
        }, {
            id: 2,
            text: 'Clear',
            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 - 2, centerY);
        this.setEndPos(centerX + 8, 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);
    },
});

/**
 * 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: '#454C51',
            stroke: '#999',
            'stroke-opacity': 0.2, // the border
        },
        blocked: {
            fill: '#25476E',
            stroke: '#2D6FC9',
            'stroke-width': 1.5,
            'stroke-opacity': 0.8
        },
        start: {
            fill: '#E5DE81',
            stroke: '#FFF',
            'stroke-width': 1,
            'stroke-opacity': 0.8
        },
        end: {
            fill: '#6CA929',
            stroke: '#B8E986',
            'stroke-opacity': 0.5
        },
        opened: {
            fill: '#47668A',
            stroke: '#86B5EC',
            opacity: 0.5,
            'stroke-opacity': 0.2,
        },
        closed: {
            fill: '#5D7B9E',
            stroke: '#FFF',
            opacity: 0.5,
            'stroke-width': 0.5,
            'stroke-opacity': 0.8
        },
        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: '#FFCF00',
        'stroke-width': 1,
    },
    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);

        //this.generateMaze();

        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();
            }
        });
    },

    generateMaze: function(callback) {

        // Generate a very simple maze that introduces the idea to the user

        var x = Controller.startX + 5;
        var y = Controller.startY + -2;

        for (var i = 5 - 1; i >= 0; i--) {
            Controller.setWalkableAt(x, y, false);

            y++;
        };
        
        
        //Controller.setWalkableAt(13, 9, false);
        //Controller.setWalkableAt(13, 10, false);
        //Controller.setWalkableAt(13, 11, false);
        //Controller.setWalkableAt(13, 12, false);

        //console.log(maze)
        /* TODO: Generate Random Maze 
        maze = new MazeGenerator(30, 30);
        maze.generate();
        for (var i = 0; i < maze.graph.width; i++) {
            for(var j = 0; j < maze.graph.width; j++) {
                console.log(i, j)
                var start, end;

                View.setWalkableAt(i, j, false)
            }
        }
        */
    },

    setStartPos: function(gridX, gridY) {
        var coord = this.toPageCoordinate(gridX, gridY);
        if (!this.startNode) {
            this.startNode = this.paper.image(
                "https://s3-us-west-2.amazonaws.com/s.cdpn.io/230399/start.png",
                coord[0],
                coord[1],
                28,
                28
            ).attr(this.nodeStyle.normal)
             .animate(this.nodeStyle.start, 1000)
             .mouseover(function (e) {
                this[0].style.cursor = "move";                
             });
             
        } 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.image(
                "https://s3-us-west-2.amazonaws.com/s.cdpn.io/230399/end.png",
                coord[0] - 4,
                coord[1] - 4,
                40,
                40
            ).attr(this.nodeStyle.normal)
             .animate(this.nodeStyle.end, 1000)
             .mouseover(function (e) {
                this[0].style.cursor = "move";                
             });
        } 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':
            var coord = this.toPageCoordinate(gridX, gridY);
            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;
            }

            console.log(gridY, gridX)
            node = blockedNodes[gridY][gridX] = this.rects[gridY][gridX].clone();

            this.colorizeNode(node, this.nodeStyle.blocked.fill);
            node.attr(this.nodeStyle.blocked)
            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);

        var circles = [];
        
        for (var i = path.length - 1; i >= 0; i--) {
            var coord = this.toPageCoordinate(path[i][0], path[i][1]);

            /* Also draw a dot for each part of the path */
            if (!(this.startNode.attrs.x == coord[0] && this.startNode.attrs.y == coord[1])) {
            
                var circle = this.paper.image(
                "https://s3-us-west-2.amazonaws.com/s.cdpn.io/230399/dot.png",
                coord[0],
                coord[1],
                28,
                28)
                .animate(this.startNode, 1000)

                circles.push(circle)
            }
        };

        this.circles = circles;
        
    },
    /**
     * 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();
        }
        
        if (this.circles) {
            for (var i = this.circles.length - 1; i >= 0; i--) {
                console.log(this.circles[i])
                this.circles[i].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 = [
            'Path length: ' + Math.round(opts.pathLength * 100) / 100,
            'Number of moves: ' + opts.operationCount,
            'Time: ' + opts.timeSpent + 'ms'
        ];
        $('#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;
    },
};

/**
 * The control panel.
 */
var Panel = {
    init: function() {
        var $algo = $('#algorithm_panel');

        $('.panel').draggable();
        $('.accordion').accordion({
            collapsible: false,
        });
        $('.option_label').click(function() {
            $(this).prev().click();
        });
        $('#hide_instructions').click(function() {
            $('#instructions_panel').slideUp();
        });
        $('#play_panel').css({
            //top: $algo.offset().top + $algo.outerHeight() + 20
        });
        $('#button2').attr('disabled', 'disabled');
    },
    /**
     * Get the user selected path-finder.
     * TODO: clean up this messy code.
     */
    getFinder: function() {
        var finder, selected_header, heuristic, allowDiagonal, biDirectional, dontCrossCorners, weight, trackRecursion, timeLimit;
        
        selected_header = $(
            '#algorithm_panel ' +
            '.ui-accordion-header[aria-selected=true]'
        ).attr('id');
        
        switch (selected_header) {

        case 'astar_header':
            allowDiagonal = typeof $('#astar_section ' +
                                     '.allow_diagonal:checked').val() !== 'undefined';
            biDirectional = typeof $('#astar_section ' +
                                     '.bi-directional:checked').val() !=='undefined';
            dontCrossCorners = typeof $('#astar_section ' +
                                     '.dont_cross_corners:checked').val() !=='undefined';

            /* parseInt returns NaN (which is falsy) if the string can't be parsed */
            weight = parseInt($('#astar_section .spinner').val()) || 1;
            weight = weight >= 1 ? weight : 1; /* if negative or 0, use 1 */

            heuristic = $('input[name=astar_heuristic]:checked').val();
            if (biDirectional) {
                finder = new PF.BiAStarFinder({
                    allowDiagonal: allowDiagonal,
                    dontCrossCorners: dontCrossCorners,
                    heuristic: PF.Heuristic[heuristic],
                    weight: weight
                });
            } else {
                finder = new PF.AStarFinder({
                    allowDiagonal: allowDiagonal,
                    dontCrossCorners: dontCrossCorners,
                    heuristic: PF.Heuristic[heuristic],
                    weight: weight
                });
            }
            break;

        case 'breadthfirst_header':
            allowDiagonal = typeof $('#breadthfirst_section ' +
                                     '.allow_diagonal:checked').val() !== 'undefined';
            biDirectional = typeof $('#breadthfirst_section ' +
                                     '.bi-directional:checked').val() !== 'undefined';
            dontCrossCorners = typeof $('#breadthfirst_section ' +
                                     '.dont_cross_corners:checked').val() !=='undefined';
            if (biDirectional) {
                finder = new PF.BiBreadthFirstFinder({
                    allowDiagonal: allowDiagonal,
                    dontCrossCorners: dontCrossCorners
                });
            } else {
                finder = new PF.BreadthFirstFinder({
                    allowDiagonal: allowDiagonal,
                    dontCrossCorners: dontCrossCorners
                });
            }
            break;

        case 'bestfirst_header':
            allowDiagonal = typeof $('#bestfirst_section ' +
                                     '.allow_diagonal:checked').val() !== 'undefined';
            biDirectional = typeof $('#bestfirst_section ' +
                                     '.bi-directional:checked').val() !== 'undefined';
            dontCrossCorners = typeof $('#bestfirst_section ' +
                                     '.dont_cross_corners:checked').val() !=='undefined';
            heuristic = $('input[name=bestfirst_heuristic]:checked').val();
            if (biDirectional) {
                finder = new PF.BiBestFirstFinder({
                    allowDiagonal: allowDiagonal,
                    dontCrossCorners: dontCrossCorners,
                    heuristic: PF.Heuristic[heuristic]
                });
            } else {
                finder = new PF.BestFirstFinder({
                    allowDiagonal: allowDiagonal,
                    dontCrossCorners: dontCrossCorners,
                    heuristic: PF.Heuristic[heuristic]
                });
            }
            break;

        case 'dijkstra_header':
            allowDiagonal = typeof $('#dijkstra_section ' +
                                     '.allow_diagonal:checked').val() !== 'undefined';
            biDirectional = typeof $('#dijkstra_section ' +
                                     '.bi-directional:checked').val() !=='undefined';
            dontCrossCorners = typeof $('#dijkstra_section ' +
                                     '.dont_cross_corners:checked').val() !=='undefined';
            if (biDirectional) {
                finder = new PF.BiDijkstraFinder({
                    allowDiagonal: allowDiagonal,
                    dontCrossCorners: dontCrossCorners
                });
            } else {
                finder = new PF.DijkstraFinder({
                    allowDiagonal: allowDiagonal,
                    dontCrossCorners: dontCrossCorners
                });
            }
            break;

        case 'jump_point_header':
            trackRecursion = typeof $('#jump_point_section ' +
                                     '.track_recursion:checked').val() !== 'undefined';
            heuristic = $('input[name=jump_point_heuristic]:checked').val();
            
            finder = new PF.JumpPointFinder({
              trackJumpRecursion: trackRecursion,
              heuristic: PF.Heuristic[heuristic],
              diagonalMovement: PF.DiagonalMovement.IfAtMostOneObstacle
            });
            break;
        case 'orth_jump_point_header':
            trackRecursion = typeof $('#orth_jump_point_section ' +
                                     '.track_recursion:checked').val() !== 'undefined';
            heuristic = $('input[name=orth_jump_point_heuristic]:checked').val();

            finder = new PF.JumpPointFinder({
              trackJumpRecursion: trackRecursion,
              heuristic: PF.Heuristic[heuristic],
              diagonalMovement: PF.DiagonalMovement.Never
            });
            break;
        case 'ida_header':
            allowDiagonal = typeof $('#ida_section ' +
                                     '.allow_diagonal:checked').val() !== 'undefined';
            dontCrossCorners = typeof $('#ida_section ' +
                                     '.dont_cross_corners:checked').val() !=='undefined';
            trackRecursion = typeof $('#ida_section ' +
                                     '.track_recursion:checked').val() !== 'undefined';

            heuristic = $('input[name=jump_point_heuristic]:checked').val();

            weight = parseInt($('#ida_section input[name=astar_weight]').val()) || 1;
            weight = weight >= 1 ? weight : 1; /* if negative or 0, use 1 */

            timeLimit = parseInt($('#ida_section input[name=time_limit]').val());

            // Any non-negative integer, indicates "forever".
            timeLimit = (timeLimit <= 0 || isNaN(timeLimit)) ? -1 : timeLimit;

            finder = new PF.IDAStarFinder({
              timeLimit: timeLimit,
              trackRecursion: trackRecursion,
              allowDiagonal: allowDiagonal,
              dontCrossCorners: dontCrossCorners,
              heuristic: PF.Heuristic[heuristic],
              weight: weight
            });

            break;
        }

        return finder;
    }
};

External CSS

  1. https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.2/themes/smoothness/jquery-ui.css
  2. https://s3-us-west-2.amazonaws.com/s.cdpn.io/230399/jquery-ui.min.css

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js
  2. https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.2/jquery-ui.min.js
  3. https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.2/raphael-min.js
  4. https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.0.5/es5-shim.min.js
  5. https://cdnjs.cloudflare.com/ajax/libs/javascript-state-machine/2.0.0/state-machine.min.js
  6. https://cdnjs.cloudflare.com/ajax/libs/async/0.9.0/async.js
  7. https://s3-us-west-2.amazonaws.com/s.cdpn.io/230399/pathfinding-browser.min.js
  8. https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js