components/NodeMap.js

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

const
    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) {
    const
        neighbor = this.neighbors[desc];
    
    if (neighbor) {
        if (neighbor.isNode) {
            return neighbor;
        } else if (typeof neighbor === 'string') {
            const
                node = this.map.getNode(neighbor);

            if (node) {
                this.neighbors[desc] = node;
                return node;
            }
        } else if (Array.isArray(neighbor)) {
            const
                node = this.map.getNode(neighbor.join('|'));

            if (node) {
                this.neighbors[desc] = node;
                return node;
            }
        }
        return null;
    } else {
        return null;
    }
};

proto.addToNode = function (entity) {
    for (let i = 0; i < this.contains.length; i++) {
        if (this.contains[i] === entity) {
            return false;
        }
    }
    this.contains.push(entity);
    return entity;
};

proto.removeFromNode = function (entity) {
    for (let 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);

export default 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 () {
        const
            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 (let i = 0; i < map.length; i++) {
            this.addNode(Node.setUp(map[i], this));
        }
    },

    events: {
        "add-node": function (nodeDefinition) {
            let 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 (let i = this.residentsAwaitingNode.length - 1; i >= 0; i--) {
                const
                    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);
                }
            }
        },

        "child-entity-removed": function (entity) {
            if (entity.isNode) {
                this.owner.triggerEvent('remove-node', entity);
            }
        },

        "remove-node": function (node) {
            this.removeNode(node);
        }
    },
    
    methods: {
        addNode: function (node) {
            this.map.push(node);
            this.nodes[node.id] = node;
        },
        
        removeNode: function (node) {
            const
                index = this.map.indexOf(node);

            if (index >= 0) {
                this.map.splice(index, 1);
                delete this.nodes[node.id];
            } else {
                platypus.debug.warn(`NodeMap: "${node.id}" is not mapped, so it cannot be removed.`);
            }
        },
        
        destroy: function () {
            // Destroy simple node objects.
            for (let 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 (...args) {
            let id = args.join('|');
            
            if (args.length === 1) {
                if (args[0].isNode) {
                    if (args[0].destroyed) {
                        return this.nodes[args[0].id];
                    } else {
                        return args[0];
                    }
                } else if (Array.isArray(args[0])) {
                    id = args[0].join('|');
                }
            }
            
            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) {
            const
                p1 = Vector.setUp(point),
                p2 = Vector.setUp(),
                list = including || this.map;
            let closest = null,
                d = Infinity;
            
            for (let i = 0; i < list.length; i++) {
                const
                    m = p2.setVector(p1).subtractVector(list[i].position).magnitude();

                if (m < d) {
                    if (excluding?.indexOf(list[i]) >= 0) {
                        break;
                    }
                    
                    d = m;
                    closest = list[i];
                }
            }
            
            p1.recycle();
            p2.recycle();
            
            return closest;
        }
    }
});