components/NodeMap.js

import {arrayCache, greenSplice} from '../utils/array.js';
import Entity from '../Entity.js';
import Vector from '../Vector.js';
import config from 'config';
import createComponentClass from '../factory.js';
import recycle from 'recycle';

export default (function () {
    var Node = function (definition, map) { // This is a basic node object, but can be replaced by entities having a `Node` component if more functionality is needed.
            if (definition.id) {
                if (typeof definition.id === 'string') {
                    this.id = definition.id;
                } else if (Array.isArray(definition.id)) {
                    this.id = definition.id.join('|');
                } else {
                    this.id = String(Math.random());
                }
            } else {
                this.id = String(Math.random());
            }

            this.isNode = true;
            this.map = map;
            this.contains = arrayCache.setUp();
            this.type = definition.type || '';

            if (!this.position) {
                Vector.assign(this, 'position', 'x', 'y', 'z');
            }
            this.x = definition.x || 0;
            this.y = definition.y || 0;
            this.z = definition.z || 0;

            this.neighbors = definition.neighbors || {};
        },
        proto = Node.prototype;
    
    proto.getNode = function (desc) {
        var neighbor = null;
        
        if (this.neighbors[desc]) {
            neighbor = this.neighbors[desc];
            if (neighbor.isNode) {
                return neighbor;
            } else if (typeof neighbor === 'string') {
                neighbor = this.map.getNode(neighbor);
                if (neighbor) {
                    this.neighbors[desc] = neighbor;
                    return neighbor;
                }
            } else if (Array.isArray(neighbor)) {
                neighbor = this.map.getNode(neighbor.join('|'));
                if (neighbor) {
                    this.neighbors[desc] = neighbor;
                    return neighbor;
                }
            }
            return null;
        } else {
            return null;
        }
    };

    proto.add = function (entity) {
        var i = 0;
        
        for (i = 0; i < this.contains.length; i++) {
            if (this.contains[i] === entity) {
                return false;
            }
        }
        this.contains.push(entity);
        return entity;
    };
    
    proto.remove = function (entity) {
        var i = 0;
        
        for (i = 0; i < this.contains.length; i++) {
            if (this.contains[i] === entity) {
                return greenSplice(this.contains, i);
            }
        }
        return false;
    };
    
    recycle.add(Node, 'Node', Node, function () {
        arrayCache.recycle(this.contains);
    }, true, config.dev);
    
    return createComponentClass(/** @lends platypus.components.NodeMap.prototype */{
        id: 'NodeMap',
        
        publicProperties: {
            /**
             * An array of node definitions to create the NodeMap. A node definition can take the following form:
             *
             *         {
             *           "NodeId": "Node1",
             *           // A string or array that becomes the id of the Node. Arrays are joined using "|" to create the id string.
             *           "type": "path",
             *           // A string that determines the type of the node.
             *           "x": 0,
             *           // Sets the x axis position of the node.
             *           "y": 0,
             *           // Sets the y axis position of the node.
             *           "z": 0,
             *           // Sets the z axis position of the node.
             *           "neighbors": {
             *           // A list of key/value pairs where the keys are directions from the node and values are node ids.
             *             "west": "node0",
             *             "east": "node2"
             *           }
             *         }
             *
             * @property map
             * @type Array
             * @default []
             */
            map: []
        },
        
        /**
         * This component sets up a NodeMap to be used by the [NodeResident](platypus.components.NodeResident.html) component on this entity's child entities.
         *
         * @memberof platypus.components
         * @uses platypus.Component
         * @constructs
         * @listens platypus.Entity#add-node
         * @listens platypus.Entity#child-entity-added
         * @fires platypus.Entity#add-node
         */
        initialize: function () {
            var i   = 0,
                map = this.map;
            
            this.map   = arrayCache.setUp(); // Original map is node definitions, so we replace it with actual nodes.
            this.nodes = {};
            this.residentsAwaitingNode = arrayCache.setUp();
            
            for (i = 0; i < map.length; i++) {
                this.addNode(Node.setUp(map[i], this));
            }
        },

        events: {
            "add-node": function (nodeDefinition) {
                var i = 0,
                    entity = null,
                    node   = null;
                
                if (nodeDefinition.isNode) {// if it's already a node, put it on the map.
                    node = nodeDefinition;
                    nodeDefinition.map = this;
                } else {
                    node = Node.setUp(nodeDefinition, this);
                }
                
                this.addNode(node);
                
                for (i = this.residentsAwaitingNode.length - 1; i >= 0; i--) {
                    entity = this.residentsAwaitingNode[i];
                    if (node.id === entity.nodeId) {
                        greenSplice(this.residentsAwaitingNode, i);
                        entity.node = this.getNode(entity.nodeId);
                    }
                }
            },

            "child-entity-added": function (entity) {
                if (entity.isNode) {        // a node
                    /**
                     * Expects a node definition to create a node in the NodeMap.
                     *
                     * @event platypus.Entity#add-node
                     * @param definition {Object} Key/value pairs.
                     * @param definition.nodeId {String|Array} This value becomes the id of the Node. Arrays are joined using "|" to create the id string.
                     * @param definition.type {String} This determines the type of the node.
                     * @param definition.x {String} Sets the x axis position of the node.
                     * @param definition.y {String} Sets the y axis position of the node.
                     * @param definition.z {String} Sets the z axis position of the node.
                     * @param definition.neighbors {Object} A list of key/value pairs where the keys are directions from the node and values are node ids. For example: {"west": "node12"}.
                     */
                    this.owner.triggerEvent('add-node', entity);
                } else if (entity.nodeId) { // a NodeResident
                    entity.node = this.getNode(entity.nodeId);
                    if (!entity.node) {
                        this.residentsAwaitingNode.push(entity);
                    }
                }
            }
        },
        
        methods: {
            addNode: function (node) {
                this.map.push(node);
                this.nodes[node.id] = node;
            },
            
            destroy: function () {
                var i = 0;
                
                // Destroy simple node objects.
                for (i = 0; i < this.map.length; i++) {
                    if (!(this.map[i] instanceof Entity)) {
                        this.map[i].recycle();
                    }
                }
                
                arrayCache.recycle(this.map);
                arrayCache.recycle(this.residentsAwaitingNode);
            }
        },
        
        publicMethods: {
            /**
             * Gets a node by node id.
             *
             * @method platypus.components.NodeMap#getNode
             * @param id {String|Array|Node} This id of the node to retrieve. If an array or more than one parameter is supplied, values are concatenated with "|" to create a single string id. Supplying a node returns the same node (useful for processing a mixed list of nodes and node ids).
             */
            getNode: function () {
                var i       = 0,
                    id      = '',
                    divider = '',
                    args    = arguments;
                
                if (args.length === 1) {
                    if (args[0].isNode) {
                        return args[0];
                    } else if (Array.isArray(args[0])) {
                        args = args[0];
                    }
                }
                
                for (i = 0; i < args.length; i++) {
                    id += divider + args[i];
                    divider = '|';
                }

                return this.nodes[id] || null;
            },
            
            /**
             * Finds the closest node to a given point, with respect to any inclusion or exclusion lists.
             *
             * @method platypus.components.NodeMap#getClosestNode
             * @param point {platypus.Vector} A location for which a closest node is being found.
             * @param [including] {Array} A list of nodes to include in the search. If not set, the entire map is searched.
             * @param [excluding] {Array} A list of nodes to exclude from the search.
             */
            getClosestNode: function (point, including, excluding) {
                var i = 0,
                    j = 0,
                    p1 = Vector.setUp(point),
                    p2 = Vector.setUp(),
                    m = 0,
                    list = including || this.map,
                    closest = null,
                    d = Infinity;
                
                for (i = 0; i < list.length; i++) {
                    m = p2.setVector(p1).subtractVector(list[i].position).magnitude();
                    if (m < d) {
                        if (excluding) {
                            j = excluding.indexOf(list[i]);
                            if (j >= 0) {
                                break;
                            }
                        }
                        
                        d = m;
                        closest = list[i];
                    }
                }
                
                p1.recycle();
                p2.recycle();
                
                return closest;
            }
        }
    });
}());