# Source: agentmap.js

    /* The Agentmap class, which turns a Leaflet map into a simulation platform. */

    let lineSlice = require('@turf/line-slice').default,
    length = require('@turf/length').default;

    /**
     * The main class for building, storing, simulating, and manipulating agent-based models on Leaflet maps.
     *
     * @class Agentmap
     * @param {object} map - A Leaflet Map instance.
     * @param {number} [animation_interval=1] - The number of steps agents must move before being redrawn. Given 1, they will be redrawn after every step. Given 0, the animation will not update at all. 1 by default. Must be a nonnegative integer.
     * @property {object} map - A Leaflet Map instance.
     * @property {FeatureGroup} agents - A featureGroup containing all agents.
     * @property {FeatureGroup} units - A featureGroup containing all units.
     * @property {FeatureGroup} streets - A featureGroup containing all streets.
     * @property {object} state - Properties detailing the state of the simulation process.
     * @property {boolean} state.running - Whether the simulation is running or not.
     * @property {boolean} state.paused - Whether the simulation is paused.
     * @property {?number} state.animation_frame_id - The id of the agentmap's update function in the queue of functions to call for the coming animation frame.
     * @property {?number} state.ticks - The number of ticks elapsed since the start of the simulation.
     * @property {number} animation_interval - The number of steps agents must move before being redrawn. Given 1, they will be redrawn after every step. Given 0, the animation will not update at all. 1 by default. Will be a nonnegative integer.
     * @property {?function} controller - User-defined function to be called on each update.
     */
    Agentmap = function (map, animation_interval = 1) {
    	Agentmap.checkAnimIntervalOption(animation_interval);

    	this.map = map,
    	this.units = null,
    	this.streets = null,
    	this.agents = null,
    	this.pathfinder = null,
    	this.state = {
    		running: false,
    		paused: false,
    		animation_frame_id: null,
    		ticks: null,
    	},
    	this.controller = function() {},
    	this.animation_interval = animation_interval
    };

    /**
     * Change the animation interval of the simulation & redraw the agents.
     *
     * @param {number} animation_interval - The desired animation interval to give the simulation. Must be a nonnegative integer.
     */
    Agentmap.prototype.setAnimationInterval = function(animation_interval) {
    	Agentmap.checkAnimIntervalOption(animation_interval);

    	this.animation_interval = animation_interval;

    	this.agents.eachLayer(agent => agent.setLatLng(agent._latlng));
    }

    /**
     * Check whether the animation interval option provided is valid.
     * @private
     *
     * @param {number} animation_interval - An input specifying an animation interval distance.
     */
    Agentmap.checkAnimIntervalOption = function(animation_interval) {
    	if (!Number.isInteger(animation_interval) && animation_interval >= 0) {
    		throw new Error("The animation_interval must be a non-negative integer!");
    	}
    }

    /**
     * Get an animation frame, have the agents update & get ready to be drawn, and keep doing that until paused or reset.
     */
    Agentmap.prototype.run = function() {
    	if (this.state.running === false) {
    		this.state.running = true;

    		let animation_update = (function (rAF_time) {

    			if (this.state.paused === true) {
    				this.state.paused = false;
    			}
    			this.state.animation_frame_id = L.Util.requestAnimFrame(animation_update);
    			this.update();
    		}).bind(this);

    		this.state.animation_frame_id = L.Util.requestAnimFrame(animation_update);
    	}
    }

    /**
     * Update the simulation at the given time.
     * @private
     */
    Agentmap.prototype.update = function() {
    	if (this.state.ticks === null) {
    		this.state.ticks = 0;
    	}

    	//Execute user-provided per-tick instructions for the agentmap.
    	this.controller();

    	//Execute user-provided per-tick instructions for each agent.
    	this.agents.eachLayer(function(agent) {
    		agent.controller();
    	});

    	this.state.ticks += 1;
    };

    /**
    * Stop the animation, reset the animation state properties, and delete the features.
    */
    Agentmap.prototype.clear = function() {
    	L.Util.cancelAnimFrame(this.state.animation_frame_id);
    	this.state.running = false,
    	this.state.paused = false,
    	this.state.animation_frame_id = null,
    	this.state.ticks = null,

    	this.agents.clearLayers();
    	this.streets.clearLayers();
    	this.units.clearLayers();
    };

    /**
     * Stop the animation, stop updating the agents.
     */
    Agentmap.prototype.pause = function() {
    	L.Util.cancelAnimFrame(this.state.animation_frame_id);
    	this.state.running = false,
    	this.state.paused = true;
    };

    /**
     * Get a point through which an agent can exit/enter a unit.
     *
     * @param {number} unit_id - The unique ID of the unit whose door you want.
     * @returns {LatLng} - The coordinates of the center point of the segment of the unit parallel to the street.
     */
    Agentmap.prototype.getUnitDoor = function(unit_id) {
    	let unit = this.units.getLayer(unit_id);

    	if (typeof(unit) === "undefined") {
    		throw new Error("No unit with the specified ID exists.");
    	}

    	let unit_spec = unit.getLatLngs()[0],
    	corner_a = unit_spec[0],
    	corner_b = unit_spec[1],
    	door = 	L.latLngBounds(corner_a, corner_b).getCenter();

    	return door;
    };

    /**
     * Get the point on the adjacent street in front of the unit's door.
     *
     * @param {number} unit_id - The unique ID of the unit whose door's corresponding point on the street you want.
     * @returns {LatLng} - The coordinates point of the adjacent street directly in front of unit's door.
     */
    Agentmap.prototype.getStreetNearDoor = function(unit_id) {
    	let unit = this.units.getLayer(unit_id);

    	if (typeof(unit) === "undefined") {
    		throw new Error("No unit with the specified ID exists.");
    	}

    	let unit_anchors = L.A.reversedCoordinates(unit.street_anchors),
    	street_point = L.latLngBounds(...unit_anchors).getCenter();

    	return street_point;
    };

    /**
     * Given a unit and a pair of coordinates between 0 and 1, return a corresponding point inside the unit, offset from its first corner along the street.
     *
     * @param {number} unit_id - The unique ID of the unit whose interior point you want.
     * @param {number} x - A point between 0 and 1 representing a position along the width of a unit.
     * @param {number} y - A point between 0 and 1 representing a position along the depth of a unit.
     * @returns {LatLng} - The global coordinates of the specified position within the unit.
     */
    Agentmap.prototype.getUnitPoint = function(unit_id, x, y) {
    	if (x < 0 || x > 1 || y < 0 || y > 1) {
    		throw new Error("x and y must both be between 0 and 1!");
    	}

    	let unit = this.units.getLayer(unit_id),
    	unit_corners = unit.getLatLngs()[0],
    	front_right = unit_corners[0],
    	front_left = unit_corners[1],
    	back_right = unit_corners[3],
    	front_length = front_left.lng - front_right.lng,
    	side_length = back_right.lng - front_right.lng,
    	front_slope = (front_right.lat - front_left.lat) / (front_right.lng - front_left.lng),
    	side_slope = (front_right.lat - back_right.lat) / (front_right.lng - back_right.lng);

    	//Get the coordinate of the position along the front (x) axis.
    	let lng_along_front = front_right.lng + front_length * x,
    	lat_along_front = front_right.lat + (front_length * x) * front_slope,
    	point_along_front = L.latLng(lat_along_front, lng_along_front);

    	//From the position on the front axis, get the coordinate of a position along a line perpendicular to the front and
    	//parallel to the side (y) axis.
    	let lng_along_side = point_along_front.lng + side_length * y,
    	lat_along_side = point_along_front.lat + (side_length * y) * side_slope,
    	point_in_depth = L.latLng(lat_along_side, lng_along_side);

    	return point_in_depth;
    }

    /**
     * Given a point on a street, find the nearest intersection on that street (with any other street).
     *
     * @param {LatLng} lat_lng - The coordinates of the point on the street to search from.
     * @param {Place} place - A place object corresponding to the street.
     * @returns {LatLng} - The coordinates of the nearest intersection.
     */
    Agentmap.prototype.getNearestIntersection = function(lat_lng, place) {
    	let street_id,
    	street_feature;

    	if (place.type === "street") {
    		street_id = place.id;
    	}
    	else {
    		throw new Error("place must be a street!");
    	}

    	street_feature = this.streets.getLayer(street_id).toGeoJSON();

    	let intersections = this.streets.getLayer(street_id).intersections,
    	intersection_points = [],
    	intersection_distances = [];

    	for (let intersection in intersections) {
    		for (let cross_point of intersections[intersection]) {
    			let intersection_point = cross_point[0],
    			distance = lat_lng.distanceTo(intersection_point);

    			/* More precise, but slower, distance detection -- not necessary yet.
    			 	let start_coords = L.A.pointToCoordinateArray(lat_lng);
    				intersection_coords = L.A.pointToCoordinateArray(intersection_point),
    				segment = lineSlice(start_coords, intersection_coords, street_feature),
    				distance = length(segment);
    			*/

    			intersection_points.push(intersection_point);
    			intersection_distances.push(distance);
    		}
    	}

    	let smallest_distance = Math.min(...intersection_distances),
    	smallest_distance_index = intersection_distances.indexOf(smallest_distance),
    	closest_intersection_point = L.latLng(intersection_points[smallest_distance_index]);

    	return closest_intersection_point;
    }

    /**
     * Since units may take a noticeably long time to generate while typically staying the same over simulations,
     * downloadUnits makes it easy to get a JS file containing the units object, so it can be included with an
     * AgentMaps app and imported into Agentmap.buildingify so they will not need to be regenerated.
     */
    Agentmap.prototype.downloadUnits = function() {
    	let file_content = "let units_data = ",
    	units_json = this.units.toGeoJSON(20);
    	file_content += JSON.stringify(units_json),
    	file = new Blob([file_content]);

    	var element = document.createElement("a");
    	element.setAttribute("href", URL.createObjectURL(file)),
    	element.setAttribute("download", "units_data.js"),
    	element.style.display = "none";
    	document.body.appendChild(element);

    	element.click();

    	document.body.removeChild(element);
    }

    /**
     * Generates an agentmap for the given map.
     *
     * @name agentmap
     * @param {object} map - A Leaflet Map instance.
     * @returns {object} - An Agentmap instance.
     */
    function agentmapFactory(map) {
    	return new Agentmap(map);
    }

    /**
     * Returns the number of layers in a Leaflet layer group.
     *
     * @memberof L.LayerGroup
     */
    function layerCount() {
    	return this.getLayers().length;
    }

    L.LayerGroup.include({count: layerCount});

    exports.Agentmap = Agentmap,
    exports.agentmap = agentmapFactory;