components/Node.js

/**
## JSON Definition
    {
      "type": "NodeResident",
      
      "nodeId": "city-hall",
      // Optional. The id of the node that this entity should start on. Uses the entity's nodeId property if not set here.
      
      "nodes": ['path','sidewalk','road'],
      // Optional. This is a list of node types that this entity can reside on. If not set, entity can reside on any type of node.
      
      "shares": ['friends','neighbors','city-council-members'],
      // Optional. This is a list of entities that this entity can reside with on the same node. If not set, this entity cannot reside with any entities on the same node.
      
      "speed": 5,
      // Optional. Sets the speed with which the entity moves along an edge to an adjacent node. Default is 0 (instantaneous movement).
      
      "updateOrientation": true
      // Optional. Determines whether the entity's orientation is updated by movement across the NodeMap. Default is false.
    }
*/
import {arrayCache, greenSplice} from '../utils/array.js';
import Vector from '../Vector.js';
import createComponentClass from '../factory.js';

export default createComponentClass(/** @lends platypus.components.Node.prototype */{
    
    id: 'Node',

    properties: {
        /**
         * If provided, treats these property names as neighbors, assigning them to the neighbors object. For example, ["east", "west"] creates `entity.east` and `entity.west` entity properties that are pointers to those neighbors.
         *
         * @property neighborProperties
         * @type Array
         * @default null
         */
        neighborProperties: null
    },
    
    publicProperties: {
        neighbors: null,
        nodeId: '',
        x: 0,
        y: 0,
        z: 0
    },
    
    /**
     * This component causes an entity to be a position on a [[NodeMap]]. This component should not be confused with `NodeResident` which should be used on entities that move around on a NodeMap: `Node` simply represents a non-moving location on the NodeMap.
     * 
     * @memberof platypus.components
     * @uses platypus.Component
     * @constructs
     */
    initialize: function () {
        const owner = this.owner;

        this.nodeId = this.nodeId || owner.id || String(Math.random());
        
        if ((typeof this.nodeId !== 'string') && (this.nodeId.length)) {
            this.nodeId = this.nodeId.join('|');
        }
        
        owner.nodeId = this.nodeId;
        
        owner.isNode = true;
        this.map = owner.map = owner.map || owner.parent || null;
        this.contains = owner.contains = arrayCache.setUp();
        this.edgesContain = owner.edgesContain = arrayCache.setUp();
        
        Vector.assign(owner, 'position', 'x', 'y', 'z');
        
        if (!this.neighbors) {
            this.neighbors = {};
        }
        
        if (this.neighborProperties) {
            const properties = this.neighborProperties;

            for (let i = 0; i < properties.length; i++) {
                const
                    propertyName = properties[i],
                    value = owner[propertyName];

                if (value) {
                    this.neighbors[propertyName] = value;
                }
                Object.defineProperty(owner, propertyName, {
                    get: () => this.neighbors[propertyName],
                    set: (value) => {
                        if (value !== this.neighbors[propertyName]) {
                            this.neighbors[propertyName] = value;
                            for (let i = 0; i < this.contains.length; i++) {
                                this.contains[i].triggerEvent('set-directions');
                            }
                        }
                    }
                });
            }
        }
    },
    
    events: {
        "add-neighbors": function (neighbors) {
            const
                directions = Object.keys(neighbors);

            for (let i = 0; i < directions.length; i++) {
                const
                    direction = directions[i];

                this.neighbors[direction] = neighbors[direction];
            }
            
            for (let i = 0; i < this.contains.length; i++) {
                this.contains[i].triggerEvent('set-directions');
            }
        },
        "remove-neighbor": function (nodeOrNodeId) {
            const
                id = nodeOrNodeId.nodeId ?? nodeOrNodeId,
                neighbors = Object.keys(this.neighbors);

            for (let i = 0; i < neighbors.length; i++) {
                const
                    neighbor = neighbors[i];
                
                if ((neighbor === id) || (neighbor.nodeId === id)) {
                    delete this.neighbors[i];
                }
            }
        }
    },
    
    methods: {
        toJSON (entityProperties) {
            const
                neighborProperties = this.neighborProperties;

            neighborProperties.forEach((neighbor) => {
                const
                    neighborNode = this.neighbors[neighbor];

                if (neighborNode) {
                    entityProperties[neighbor] = neighborNode.id;
                }
            });

            entityProperties.nodeId = this.nodeId;

            return {
                type: 'Node',
                neighborProperties
            };
        },

        destroy () {
            arrayCache.recycle(this.contains);
            this.contains = this.owner.contains = null;
            arrayCache.recycle(this.edgesContain);
            this.edgesContain = this.owner.edgesContain = null;
        }
    },
    
    publicMethods: {
        joinNode (node, direction, mutual = false) {
            this.neighbors[direction] = node;
            
            for (let i = 0; i < this.contains.length; i++) {
                this.contains[i].triggerEvent('set-directions');
            }

            if (mutual) {
                node.joinNode(this.owner, (typeof mutual === 'boolean') ? direction : mutual);
            }
        },

        /**
         * Gets a neighboring node Entity.
         * 
         * @memberof Node.prototype
         * @param {String} desc Describes the direction to check.
         * @returns {platypus.Entity}
         */
        getNode: function (desc) {
            const
                neighbor = this.neighbors[desc];

            //map check
            if (!this.map && this.owner.map) {
                this.map = this.owner.map;
            }
            
            if (neighbor) {
                const
                    testNeighbor = neighbor.destroyed ? neighbor.id : neighbor;

                if (testNeighbor.isNode) {
                    return testNeighbor;
                } else if (typeof testNeighbor === 'string') {
                    const
                        actualNeighbor = this.map.getNode(testNeighbor);
                    
                    if (actualNeighbor) {
                        this.neighbors[desc] = actualNeighbor;
                        return actualNeighbor;
                    }
                } else if (testNeighbor.length) {
                    const
                        actualNeighbor = this.map.getNode(testNeighbor.join('|'));

                    if (actualNeighbor) {
                        this.neighbors[desc] = actualNeighbor;
                        return actualNeighbor;
                    }
                }
            }

            return null;
        },

        /**
         * Puts an entity on this node.
         * 
         * @memberof Node.prototype
         * @param {platypus.Entity} entity
         * @returns {platypus.Entity}
         */
        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;
        },

        /**
         * Removes an entity from this node.
         * 
         * @memberof Node.prototype
         * @param {platypus.Entity} entity
         * @returns {platypus.Entity}
         */
        removeFromNode: function (entity) {
            for (let i = 0; i < this.contains.length; i++) {
                if (this.contains[i] === entity) {
                    return greenSplice(this.contains, i);
                }
            }
            return false;
        },

        /**
         * Adds an entity to this node's edges.
         * 
         * @memberof Node.prototype
         * @param {platypus.Entity} entity
         * @returns {platypus.Entity}
         */
        addToEdge: function (entity) {
            for (let i = 0; i < this.edgesContain.length; i++) {
                if (this.edgesContain[i] === entity) {
                    return false;
                }
            }
            this.edgesContain.push(entity);
            return entity;
        },

        /**
         * Removes an entity from this node's edges.
         * 
         * @memberof Node.prototype
         * @param {platypus.Entity} entity
         * @returns {platypus.Entity}
         */
        removeFromEdge: function (entity) {
            for (let i = 0; i < this.edgesContain.length; i++) {
                if (this.edgesContain[i] === entity) {
                    return greenSplice(this.edgesContain, i);
                }
            }
            return false;
        }
    }
});