/* 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 = "var 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);
}
/**
* Since street layers may take a noticeably long time to generate while typically staying the same over simulations,
* downloadStreets makes it easy to get a JS file containing the streets 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.downloadStreets = function() {
let file_content = "var streets_data = ",
streets_json = this.streets.toGeoJSON(20);
file_content += JSON.stringify(streets_json),
file = new Blob([file_content]);
var element = document.createElement("a");
element.setAttribute("href", URL.createObjectURL(file)),
element.setAttribute("download", "streets_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;