Source: ptolemy/actor/lib/jjs/modules/mapManager/mapManager.js

// Below is the copyright agreement for the Ptolemy II system.
//
// Copyright (c) 2015-2016 The Regents of the University of California.
// All rights reserved.
//
// Permission is hereby granted, without written agreement and without
// license or royalty fees, to use, copy, modify, and distribute this
// software and its documentation for any purpose, provided that the above
// copyright notice and the following two paragraphs appear in all copies
// of this software.
//
// IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
// ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
// THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
//
// THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
// PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
// CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
// ENHANCEMENTS, OR MODIFICATIONS.
//
//
// Ptolemy II includes the work of others, to see those copyrights, follow
// the copyright link on the splash page or see copyright.htm.


/**  Map Manager
 *  @author Matt Weber
 *  @version $$Id$$
 */

// Stop extra messages from jslint and jshint.  Note that there should
// be no space between the / and the * and global. See
// https://chess.eecs.berkeley.edu/ptexternal/wiki/Main/JSHint */
/*globals addInputHandler, console, error, exports, get, input, output, parameter, require, send */
/*jshint globalstrict: true*/
'use strict';

//******************************************************************************************
// Global MapManager Variables
//******************************************************************************************

//Todo: Perhaps add a collection of MapSources to iterate through for consistancy checking?

//Hash mapping "map._key()" strings to "map objects"
//This is a simple hash
var maps = {};

//Todo: delete this? I made the decision to maintain each alias's entities with the entity.
//Hash mapping source entities with their destination alter ego. he pairs are stored as parameters with the value true.
//This is a keyed hash
//var entityAliases = {};

//Todo: This isn't a structure you could iterate through if you wanted to make sure
// all transitive coordinate transformation functions could be determined. Make it a
// two level hash perhaps? Or attach the available transformations to a coordinate system
// object

//Hash mapping (domainName, codomainName) pairs as keys to functions
//This is a keyed hash
var coordinateTransformations = {};

//Hash mapping entity names to its entity
//this is a simple hash
var entities = {};


//******************************************************************************************
//Private helper functions
//******************************************************************************************


//Assumes the key for the hash is an ordinary string
function _simpleHashToString(hash) {

    //This closure exists to protect the scope of "text" and i and hashNames when there is a recursive call
    //from the toString method below.
    return function () {

        var hashNames = Object.keys(hash);
        var text = "{ ";
        for (var i = 0; i < hashNames.length; i++) {
            text += hashNames[i] += ": " + hash[hashNames[i]].toString();
            if (i != (hashNames.length - 1)) {
                text += ", ";
            }
        }
        text += " }";

        return text;
    }();
}

//Assumes the key for the hash is a specially formatted string
function _keyedHashToString(hash) {

    //This closure exists to protect the scope of "text" and i and hashNames when there is a recursive call
    //from the toString method below.
    return function () {
        var text = "{ ";
        var hashNames = Object.keys(hash);

        for (var i = 0; i < hashNames.length; i++) {
            text += _keyToStrings(hashNames[i]) + ": " + hash[hashNames[i]].toString();
            if (i != hashNames.length - 1) {
                text += ", ";
            }
        }

        text += " }";
        return text;
    }();
}


//argument must be an array of at least two strings
//returns a string formatted with "x_" for arguments and "  " as a delimiter
function _stringsToKey(stringArray) {
    var x;
    if (!(stringArray instanceof Array)) {
        throw "Incorrect arguments to _stringsToKey. Input is not an array";
    }
    for (x in stringArray) {
        if (typeof x !== "string") {
            throw "Incorrect arguments to _stringsToKey. Input is not an array of strings.";
        }
    }
    if (stringArray.length < 2) {
        throw "Incorrect arguments to _stringsToKey.";
    }

    var key = "";
    var arg;
    var c;

    for (arg = 0; arg < stringArray.length; arg++) {

        var word = stringArray[arg];
        for (c = 0; c < word.length; c++) {
            key += ('_' + word.charAt(c));
        }
        if (arg != stringArray.length - 1) {
            key += "  ";
        }
    }

    return key;
}

//argument is a string formatted with "x_" for arguments and "  " as a delimiter
//returns an array of strings
function _keyToStrings(key) {
    if (typeof key !== "string") {
        throw "Incorrect arguments to _keyToStrings.";
    }

    if (key.indexOf("  ") == -1) {
        throw "key in _keyToStrings does not have a delimiter.";
    }

    var c;
    var c_last = "";
    var args = [];
    var arg = "";


    var state = "parse";
    var nextState;

    for (var i = 0; i < key.length; i++) {
        c = key.charAt(i);
        if (state === "parse") {
            if (c === "_") {
                nextState = "append";
            } else if (c === " ") {
                nextState = "push";
            }
        }

        if (state === "append") {
            arg += c;
            nextState = "parse";
        }

        if (state === "push") {
            args.push(arg);
            arg = "";
            nextState = "parse";
            //Note that the current value of c is ignored.
        }
    }
    args.push(arg);
    return args;
}

//REMOVED due to change of entity aliases as being stored with the entity

// function _entityAliasKey(sourceEntity, destinationEntity ) {
//          if (! ((sourceEntity instanceof Entity) && (destinationEntity instanceof Entity))  ) {
//                  throw "Incorrect arguments to _EntityAliasKey.";
//          }

//          var sourceString = sourceEntity.toString();
//          var destinationString = destinationEntity.toString();
//          return _stringsToKey([sourceString, destinationString ]);
// }

function __coordinateTransformationKey(domainName, codomainName) {
    if (!(typeof domainName === "string" && typeof codomainName === "string")) {
        throw "Incorrect arguments to _coordinateTransformationKey";
    }

    return _stringsToKey([domainName, codomainName]);

}

//******************************************************************************************
//Public Enums
//******************************************************************************************

//
/**
 * @enum
 */

exports.SpaceTypeEnum = {
    EUCLIDEAN: "Euclidean",
    METRIC: "Metric",
    TOPOLOGICAL: "Topological",
    SET: "Set"
};




//******************************************************************************************
//Public toJSONString functions for MapManager hashes
//******************************************************************************************

/**
 * Returns the current state of the global in-memory maps storage as a JSON string.
 * The object has properties: maps, coordinateTransformations, and entities.
 * @function
 * @returns {string}
 */
exports.localRepoToJSONString = function () {
    var data = {
        "maps": maps,
        "coordinateTransformations": coordinateTransformations,
        "entities": entities
    };

    return JSON.stringify(data);
};


/**
 * Returns the current state of the global maps storage as a JSON string.
 * @function
 * @returns {string}
 */
exports.mapsToJSONString = function () {
    return JSON.stringify(maps);
};

/**
 * Returns the current state of the global coordinate transformations storage as a JSON string.
 * @function
 * @returns {string}
 */
exports.coordinateTransformationsToJSONString = function () {
    return JSON.stringify(coordinateTransformations);
};


/**
 * Returns the current state of the global entities storage as a JSON string.
 * @function
 * @returns {string}
 */
exports.entitiesToJSONString = function () {
    return JSON.stringify(entities);
};

//******************************************************************************************
//Public toString functions for MapManager hashes
//******************************************************************************************

/**
 * Returns the current state of the global maps storage as a string.
 * @function
 * @returns {string}
 */
exports.mapsToString = function () {
    return _simpleHashToString(maps);
};

/**
 * Returns the current state of the global coordinate transformations storage as a string.
 * @function
 * @returns {string}
 */
exports.coordinateTransformationsToString = function () {
    return _keyedHashToString(coordinateTransformations);
};


/**
 * Returns the current state of the global entities storage as a string.
 * @function
 * @returns {string}
 */
exports.entitiesToString = function () {
    return _simpleHashToString(entities);
};

//******************************************************************************************
//Data Control Functions
//******************************************************************************************


/**
 * Deletes all maps, entities, and coordinate transformations in the repo.
 * @function
 * @returns {void} - Clearing the repo always works.
 */
exports.clearRepo = function () {
    maps = {};
    coordinateTransformations = {};
    entities = {};
    return;
};


//Todo - Validate the input map before replacing the repo.
/**
 * Replaces the current values of maps, coordinateTransformations,
 * and entities with the input. Currently does NOT check the input besides
 * making sure it has "maps", "coordinateTransformations", and "entities"
 * properties
 * @function
 * @param {String} repo - A JSON string of the format you would get calling
 * localRepoToJSONString()
 * @returns {boolean} if the input was an acceptably formatted repo.
 */
exports.replaceRepo = function (repo) {
    if (typeof name !== "string") {
        throw "Incorrect arguments to replaceRepo.";
    }

    var receivedRepo;
    try {
        receivedRepo = JSON.parse(repo);
    } catch (e) {
        if (e instanceof SyntaxError) {
            return false;
        } else {
            throw "JSON.parse threw a non-SyntaxError exception";
        }

    }

    if (!(receivedRepo.hasOwnProperty("maps") && receivedRepo.hasOwnProperty("coordinateTransformations") && receivedRepo.hasOwnProperty("entities"))) {
        return false;
    } else {
        maps = receivedRepo.maps;
        coordinateTransformations = receivedRepo.coordinateTransformations;
        entities = receivedRepo.entities;
        return true;
    }
};

//******************************************************************************************
//Entity Specific functions
//******************************************************************************************

//Todo what about other ontological metadata?
/**
 * @constructor
 * @param {string} name - The name of the entity
 */

function Entity(name) {
    if (typeof name !== "string") {
        throw "Incorrect arguments to Entity constructor.";
    }

    this.name = name;
    this.containingMap = null;
    this.aliases = {};
    this.placements = {};
    this.occupancies = {};

    this._key = function () {
        return this.name;
    };

    /**
     * @function
     * @returns {string} string representation of this object.
     */
    this.toString = function () {
        return "{ " + "name: " + this.name + " }";
    };

    /**
     * @function
     * @returns {string} string representation of this entity's aliases.
     */
    this.aliasesToString = function () {
        return _simpleHashToString(this.aliases);
    };

    //Todo should this be registerAlias instead? What counts as registering or adding?
    /**
     * Indicate that an entity has another name on another map.
     * @function
     * @param {Entity} alias - The Entity on the different map
     * @returns {boolean} if the entity alias was successfully added returns true, false otherwise.
     */
    this.addAlias = function (alias) {
        if (!(alias instanceof Entity)) {
            throw "Incorrect arguments to addAlias.";
        }

        //Check to see if this entity has been registered.
        if (!entities.hasOwnProperty(alias._key())) {
            throw "This entity is unregistered." + this.toString() + " Cannot give an unregistered entity an alias.";
        }

        //Check to see if alias for this entity has been registered. If not, throw exception.
        if (!entities.hasOwnProperty(alias._key())) {
            throw "Attempt to add an unregistered entity as an alias to " + this.toString();
        }

        //Todo, check to make sure this logic is correct, I realize aliases can exist in code too
        if ((alias === this) || this.aliases.hasOwnProperty(alias._key())) {
            return false;
        } else {
            this.aliases[alias._key()] = true;
            return true;
        }
    };

    /**
     * Specify the location of an entity that is on a map
     * @function
     * @param {Placement} placement - The location of the entity with respect to its map
     * @returns {boolean} true if the location was successfully set, false otherwise.
     */
    this.setPlacement = function (placement) {
        if (!(placement instanceof Placement)) {
            throw "Incorrect arguments to setPosition.";
        }

        //Check to see if this entity has been registered.
        if (!entities.hasOwnProperty(this._key())) {
            throw "This entity is unregistered." + this.toString() + " Cannot give an unregistered entity a position.";
        }

        if (this.containingMap === null) {
            throw "This entity has not been placed on a map." + this.toString();
        }

        if (this.placements.hasOwnProperty(placement._key())) {
            return false;
        } else {
            this.placements[placement._key()] = placement;
            return true;
        }
    };

    /**
     * Getter for placements
     * @function
     * @returns {object} Hash mapping placement keys to placements.
     */
    this.getPlacements = function () {
        return this.placements;
    };

    /**
     * Getter for name
     * @function
     * @returns {string} Name of this entity.
     */
    this.getName = function () {
        return this.name;
    };

    /**
     * Specify the location of an entity that is on a map
     * @function
     * @param {Occupancy} occupancy - The location of the entity with respect to its map
     * @returns {boolean} true if the location was successfully set, false otherwise.
     */
    this.setOccupancy = function (occupancy) {
        if (!(occupancy instanceof Occupancy)) {
            throw "Incorrect arguments to setPosition.";
        }

        //Check to see if this entity has been registered.
        if (!entities.hasOwnProperty(this._key())) {
            throw "This entity is unregistered." + this.toString() + " Cannot give an unregistered entity a position.";
        }

        if (this.containingMap === null) {
            throw "This entity has not been placed on a map." + this.toString();
        }

        if (this.occupancies.hasOwnProperty(occupancy._key())) {
            return false;
        } else {
            this.occupancies[occupancy._key()] = occupancy;
            return true;
        }
    };
}

exports.Entity = Entity;

//Note that for now entities cannot be unregistered.
/**
 * Before any entity can be used by mapManager, it must first be registered so it can be placed in the global entity storage.
 * @function
 * @param {Entity} entity - The entity object to be registered by the map manager.
 * @returns {boolean} if the entity was successfully registered returns true, false otherwise.
 */
exports.registerEntity = function (entity) {
    if (!(entity instanceof Entity)) {
        throw "Incorrect arguments to registerEntity.";
    }

    if (entities.hasOwnProperty(entity._key())) {
        return false;
    } else {
        entities[entity._key()] = entity;
        return true;
    }
};



//Removed because EntityAliases no longer exists

// exports.removeEntityAlias = function(sourceEntity, destinationEntity ) {
//          if (! ((sourceEntity instanceof Entity) && (destinationEntity instanceof Entity))  ) {
//                  throw "Incorrect arguments to EntityAlias constructor.";
//          }

//         var key = _entityAliasKey(sourceEntity, destinationEntity)

//         if (entityAliases.hasOwnProperty(key)) {
//                 delete entityAliases[key];
//                 return true;
//         } else {
//                 return false;
//         }
// }

//Removed because EntityAliases no longer exists

// exports.entityAliasExists = function(sourceEntity, destinationEntity ) {
//          if (! ((sourceEntity instanceof Entity) && (destinationEntity instanceof Entity))  ) {
//                  throw "Incorrect arguments to entityAliasExists.";
//          }

//          var key = _entityAliasKey(sourceEntity, destinationEntity)

//          if (entityAliases.hasOwnProperty(key)) {
//                  return true;
//          } else {
//                  return false;
//          }
// }

//******************************************************************************************
//CoordinateTransformation Specific functions
//******************************************************************************************


//Todo: Remove this class and just treat coordinate systems as names. Or find
//a reason to make this a full-blown object.
/**
 * @constructor
 * @param {name} name - The name of the coordinate system.
 */
function CoordinateSystem(name) {
    if (typeof name !== "string") {
        throw "Incorrect arguments to CoordinateSystem constructor.";
    }

    this.name = name;

    /**
     * @function
     * @returns {string} string representation of this object.
     */
    this.toString = function () {
        return name;
    };
}

exports.CoordinateSystem = CoordinateSystem;

//Todo should the domainName and codomainName be strings or actual coordinate system objects?
//Todo break the assumption that there only has to be one transformation from a particular domain to codomain.
/**
 * Before any coordinate transformation can be used by mapManager,
 * it must first be registered so it can be placed in the global coordinate transformation storage.
 * @function
 * @param {String} domainName - The name of domain coordinate system.
 * @param {String} codomainName - The name of codomain coordinate system.
 * @param {function} codomainName - A function that takes one argument,
 * a point in the domain, and returns a point in the codomain
 * @returns {boolean} if the coordinate transformation was successfully registered returns true, false otherwise.
 */


exports.registerCoordinateTransformation = function registerCoordinateTransformation(domainName, codomainName, transformation) {
    if (!(typeof domainName === "string" && typeof codomainName === "string" &&
            typeof transformation === "function")) {
        throw "Incorrect arguments to addCoordinateTransformation constructor.";
    }

    var key = __coordinateTransformationKey(domainName, codomainName);

    if (coordinateTransformations.hasOwnProperty(key)) {
        return false;
    } else {
        coordinateTransformations[key] = transformation;
        return true;
    }
};

/**
 * Retrieve a previously registered coordinate transformation from the global storage.
 * @function
 * @param {String} domainName - The name of domain coordinate system.
 * @param {String} codomainName - The name of codomain coordinate system.
 * @returns {boolean} A function that takes one argument,
 * a point in the domain, and returns a point in the codomain
 */
exports.getCoordinateTransformation = function getCoordinateTransformation(domainName, codomainName) {
    if (!(typeof domainName === "string" && typeof codomainName === "string")) {
        throw "Incorrect arguments to getCoordinateTransformation constructor.";
    }

    var key = __coordinateTransformationKey(domainName, codomainName);

    return coordinateTransformations[key];
};

//Todo, think through the possible messy complications of unregistering a coordinate transformation.
//This might be impossible.
//exports.unregisterCoordinateTransformation

//******************************************************************************************
//Maps Specific functions
//******************************************************************************************



/**

 * @typedef {Map}
 * @constructor
 * @param {mapName} string - The name of the map.
 * @param {SpaceTypeEnum} spaceTypeEnum - The variety of mathematical space this map considers.
 * @param {CoordinateSystem} coordinateSystem - The coordinate system with respect to which this map gives position.
 */
function Map(mapName, spaceType, coordinateSystem) {

    //Todo find a way to type check spaceType against SpaceTypeEnum
    if (!((coordinateSystem instanceof CoordinateSystem) && (typeof mapName === "string"))) {
        throw "Incorrect arguments to map constructor";
    }
    this.spaceType = spaceType;
    this.coordinateSystem = coordinateSystem;
    this.mapName = mapName;
    this.mapEntities = {}; //set of keys for entities this map contains

    this._key = function () {
        return mapName;
    };

    this.mapEntitiesToString = function () {
        return _simpleHashToString(this.mapEntities);
    };

    /**
     * @function
     * @returns {string} string representation of this object.
     */
    this.toString = function () {
        return "{ mapName: " + mapName + ", spaceType: " + this.spaceType + ", coordinateSystem: " + this.coordinateSystem + " }";
    };

    /**
     * Attach an entity to this map. This does not entail giving it a position.
     * @function
     * @param {Entity} entity - The entity to be attached to this map
     * @returns {boolean} if successful returns true, false otherwise.
     */
    this.addEntity = function (entity) {
        if (!(entity instanceof Entity)) {
            throw "Incorrect arguments to Map.addEntity.";
        }

        if (!entities.hasOwnProperty(entity._key())) {
            throw "Cannot add unregistered entity to map" + entity.toString();
        }

        if (!maps.hasOwnProperty(this._key())) {
            throw "Cannot add an entity to an unregistred map" + this.toString();
        }

        if (this.mapEntities.hasOwnProperty(entity._key()) || entity.containingMap !== null) {
            //The entity is already on this map or has already been assigned to a different map.
            return false;

        } else {
            this.mapEntities[entity._key()] = true;
            entity.containingMap = this._key();
            return true;
        }
    };


    //Todo: should it be allowed to remove an entity?
    //Todo: what happens if an entity is in an invalid state of not thinking it's on a map it is on?
    //right now this function will just return false.
    /**
     * Unattach an entity from this map.
     * @function
     * @param {Entity} entity - The entity to be unattached from this map
     * @returns {boolean} if successful returns true, false otherwise.
     */
    this.removeEntity = function (entity) {
        if (!(entity instanceof Entity)) {
            throw "Incorrect arguments to Map.addEntity.";
        }

        if (!entities.hasOwnProperty(entity._key())) {
            throw "Cannot remove an unregistered entity from the map" + entity.toString();
        }

        if (!maps.hasOwnProperty(this._key())) {
            throw "Cannot remove an entity from an unregistred map" + this.toString();
        }

        if ((!this.mapEntities.hasOwnProperty(entity._key())) || (entity.containingMap !== this._key())) {
            //Entity had not been previously assigned to this map.
            return false;

        } else {
            delete this.mapEntities[entity._key()];
            entity.containingMap = null;
            return true;
        }
    };


    /**
     * Create an SVG format image of the entities on the map, from placements having
     * 2D center coordinates.
     * @function
     * @param {number} width - The pixel width of the image.
     * @param {number} width - The pixel height of the image.
     * @returns {string} An SVG image displaying the contents of the map.
     */
    this.mapEntitiesToSVG = function (width, height) {
        if (!((typeof width === "number") && (typeof height === "number"))) {
            throw "Incorrect arguments to mapEntitiesToSVG";
        }

        console.log("function is called");

        var svgString = "";

        //Set xml version
        svgString += '<?xml version="1.0"?>\n';

        //Add a comment to show this program created the image
        svgString += '<!-- Created by mapManager -->\n';

        //Set width and height
        svgString += '<svg viewbox="0 0 100 100" preserveAspectRatio="xMidYMid meet" ';
        svgString += 'width="' + width.toString() + '" height="' + height.toString() + '">\n';

        //Map boarder
        svgString += '<rect width="100" height="100" x="0" y="0" ';
        svgString += 'style="fill:rgb(0,0,0);stroke-width:1;stroke:rgb(0,0,0)" fill-opacity="0.1" />\n';

        console.log(this.mapEntities);

        //SVG content
        for (var eKey in this.mapEntities) {
            var e = entities[eKey];
            var placements = e.getPlacements();

            console.log("eKey is:" + eKey);
            console.log("e is:" + e);
            console.log("placements is:" + placements);
            console.log("placement properties:" + Object.getOwnPropertyNames(e.getPlacements));
            for (var pKey in placements) {
                var p = placements[pKey];
                console.log("P is:" + p);
                var center = p.getCenter();
                //Only consider 2D coordinates
                console.log("center is" + center);
                if (center.length == 2) {

                    //TODO fix this! This is wildly incorrect because of different coord systems for svg
                    //I'm just doing it now for testing.
                    var cx = center[0];
                    var cy = center[1];
                    svgString += '<circle cx="' + cx.toString() + '" cy="' + cy.toString() + '" r=".5"/>\n';
                    svgString += '<text x="' + cx.toString() + '" y="' + (cy - 1).toString() +
                        '" font-family="Verdana" font-size="2">\n'; //note the -1 y

                    svgString += e.getName() + '\n';
                    svgString += '</text>\n';
                }
            }
        }

        //close SVG
        svgString += '</svg>';

        return svgString;
    };
}

exports.Map = Map;


/**
 * Before any map can be used by mapManager, it must first be registered so it can be placed in the global map storage.
 * @function
 * @param {Map} map - The map object to be registered by the map manager.
 * @returns {boolean} if the map was successfully registered returns true, false otherwise.
 */
exports.registerMap = function (map) {
    if (!(map instanceof Map)) {
        throw "Incorrect arguments to registerMap.";
    }

    if (map === undefined) {
        throw "undefined argument to registerMap.";
    }

    if (maps.hasOwnProperty(map._key())) {
        return false;
    } else {
        maps[map._key()] = map;
        return true;
    }
};


//******************************************************************************************
//Metadata classes
//******************************************************************************************

//Todo, perhaps add a cyrptopgrahic signiture to this. And maybe more information.
/**
 * @typedef {MapSource}
 * @constructor
 * @param {string} name - The name of this location information provider.
 */
function MapSource(source) {

    if (typeof source !== "string") {
        throw "Incorrect arguments to MapSource constructor";
    }

    this.name = source;

    this._key = function () {
        return this.name;
    };
}

exports.MapSource = MapSource;

//******************************************************************************************
//Position Specific Functions
//******************************************************************************************

/**
 * class describing positions and relations
 * @typedef {ObservationMetadata}
 * @constructor
 * @param {provenance} MapSource - The name of the source for this observation.
 * @param {timestamp} Date - The UTC timestamp for this observation (the time it happened, not when it was recorded).
 */
function ObservationMetadata(provenance, timestamp) {

    if (!((provenance instanceof MapSource) && (typeof timestamp === "number"))) {
        throw "Incorrect arguments to ObservationMetadata constructor";
    }

    this.provenance = provenance;
    this.timestamp = timestamp;

    this._key = function () {
        return _stringsToKey([provenance._key(), this.timestamp.toString()]);
    };
}

exports.ObservationMetadata = ObservationMetadata;



var _globalPlacementID = 0;
//Todo: so far this placement only makes sense for 2D eculidean spaces because of shape,
// and only for euclidean spaces because of pose. Generalize it!
//Todo: DCEL shape input
//Todo: Probability. Particles?

/**
 * @typedef {Placement}
 * @constructor
 * @param {ObservationMetadata} metadata - Observation data for this map.
 * @param {array} center - The coordinate position describing the center of this entity.
 * @param {Quaternion} pose - The pose of the entity.
 * @param {array} shape - A counterclockwise array of coordinates around the entity's boundary.
 * Coordinates are given as they would be when the center is at the origin and pose is in the direction
 * of the positive y-axis and flat on the x-y plane.
 * This is done so uncertainty in the location of the center doesn't contaminate the shape of the entity.
 */
function Placement(metadata, center, pose, shape) {

    if (!((metadata instanceof ObservationMetadata) && (center instanceof Array) &&
            (pose instanceof Quaternion) && (shape instanceof Array))) {
        throw "Incorrect arguments to Placment constructor";
    }

    this.metadata = metadata;
    this.center = center;
    this.pose = pose;
    this.shape = shape;
    this._placementID = _globalPlacementID++;

    this._key = function () {
        return _stringsToKey([this._placementID.toString(), this.metadata._key()]);
    };

    /**
     * Getter for center
     * @function
     * @returns {array} The coordinate position describing the center of this entity.
     */
    this.getCenter = function () {
        return this.center;
    };
}

exports.Placement = Placement;


/**
 * @typedef {Quaternion}
 * @constructor
 * @param {number} w
 * @param {number} x
 * @param {number} y
 * @param {number} z
 */
function Quaternion(w, x, y, z) {

    if (!((typeof w === "number") && (typeof x === "number") && (typeof y === "number") &&
            (typeof z === "number"))) {
        throw "Incorrect arguments to Quaternion constructor";
    }

    this.w = w;
    this.x = x;
    this.y = y;
    this.z = z;

}

exports.Quaternion = Quaternion;

var _globalOccupancyID = 0;

/**
 * @typedef {Occupancy}
 * @constructor
 * @param {metadata} ObservationMetadata - Observation data for this map.
 * @param {grid} array - An occupancy grid of 1 to 3 dimensions. Values must be integers between 0 to 100.
 */
function Occupancy(metadata, grid) {

    if (!((metadata instanceof ObservationMetadata) && (grid instanceof Array))) {
        throw "Incorrect arguments to Occupancy constructor";
    }

    this.metadata = metadata;
    this.grid = grid;
    this.dimensions = [];
    this._occupancyID = _globalOccupancyID++;

    var dataTemp = grid;
    while (dataTemp instanceof Array) {
        this.dimensions.push(dataTemp.length);
        dataTemp = dataTemp[0];
    }

    this._key = function () {
        return _stringsToKey([this._occupancyID.toString(), this.metadata._key()]);
    };

}

exports.Occupancy = Occupancy;

//******************************************************************************************
//Relation Specific Functions
//******************************************************************************************