AgentMaps/devdocs/agents.js.html
noncomputable f901ff532c Updates
2018-08-01 23:53:56 -04:00

525 lines
22 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: agents.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: agents.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>let centroid = require('@turf/centroid').default,
buffer = require('@turf/buffer').default,
booleanPointInPolygon = require('@turf/boolean-point-in-polygon').default,
along = require('@turf/along').default,
nearestPointOnLine = require('@turf/nearest-point-on-line').default,
lineSlice = require('@turf/line-slice').default,
Agentmap = require('./agentmap').Agentmap,
encodeLatLng = require('./routing').encodeLatLng;
/* Here we define agentify, the agent base class, and all other functions and definitions they rely on. */
/**
* User-defined callback that gives a feature with appropriate geometry and properties to represent an agent.
*
* @callback agentFeatureMaker
* @param {number} i - A number used to determine the agent's coordinates and other properties.
* @returns {?Point} - Either a GeoJSON Point feature with properties and coordinates for agent i, including
* a "place" property that will define the agent's initial agent.place; or null, which will cause agentify
* to immediately stop its work &amp; terminate.
*/
/**
* A standard {@link agentFeatureMaker} callback, which sets an agent's location as the center of a unit on the map.
*
* @memberof Agentmap
* @type {agentFeatureMaker}
*/
function seqUnitAgentMaker(i){
if (i > this.units.getLayers().length - 1) {
return null;
}
let unit = this.units.getLayers()[i],
unit_id = this.units.getLayerId(unit),
center_point = centroid(unit.feature);
center_point.properties.place = {"unit": unit_id},
center_point.properties.layer_options = {radius: .5, color: "red", fillColor: "red"};
return center_point;
}
/**
* Generate some number of agents and place them on the map.
*
* @param {number} count - The desired number of agents.
* @param {agentFeatureMaker} agentFeatureMaker - A callback that determines an agent i's feature properties and geometry (always a Point).
*/
function agentify(count, agentFeatureMaker) {
let agentmap = this;
if (!(this.agents instanceof L.LayerGroup)) {
this.agents = L.layerGroup().addTo(this.map);
}
let agents_existing = agentmap.agents.getLayers().length;
for (let i = agents_existing; i &lt; agents_existing + count; i++) {
//Callback function aren't automatically bound to the agentmap.
let boundFeatureMaker = agentFeatureMaker.bind(agentmap),
feature = boundFeatureMaker(i);
if (feature === null) {
return;
}
let coordinates = L.A.reversedCoordinates(feature.geometry.coordinates),
place = feature.properties.place,
layer_options = feature.properties.layer_options;
//Make sure the agent feature is valid and has everything we need.
if (!L.A.isPointCoordinates(coordinates)) {
throw new Error("Invalid feature returned from agentFeatureMaker: geometry.coordinates must be a 2-element array of numbers.");
}
else if (typeof(place.unit) !== "number" &amp;&amp;
typeof(place.street) !== "number") {
throw new Error("Invalid feature returned from agentFeatureMaker: properties.place must be a {unit: unit_id} or {street: street_id} with an existing layer's ID.");
}
new_agent = agent(coordinates, layer_options, agentmap);
new_agent.place = place;
this.agents.addLayer(new_agent);
}
}
/**
* The main class representing individual agents, using Leaflet class system.
* @private
*
* @class Agent
*/
let Agent = L.Layer.extend({});
/**
* Constructor for the Agent class, using Leaflet class system.
*
* @name Agent
* @constructor
* @param {LatLng} lat_lng - A pair of coordinates to place the agent at.
* @param {Object} options - An array of options for the agent, namely its layer.
* @param {Agentmap} agentmap - The agentmap instance in which the agent exists.
* @property {number} feature.AgentMap_id - The agent's instance id, so it can be accessed from inside the Leaflet layer. To avoid putting the actual instance inside the feature object.
* @property {Agentmap} agentmap - The agentmap instance in which the agent exists.
* @property {Place} place - A place object containing the id of the place (unit, street, etc.) where the agent is currently at.
* @property {Object} travel_state - Properties detailing information about the agent's trip that change sometimes, but needs to be accessed by future updates.
* @property {boolean} travel_state.traveling - Whether the agent is currently on a trip.
* @property {?Point} travel_state.current_point - The point where the agent is currently located.
* @property {?Point} travel_state.goal_point - The point where the agent is traveling to.
* @property {?number} travel_state.lat_dir - The latitudinal direction. -1 if traveling to lower latitude (down), 1 if traveling to higher latitude (up).
* @property {?number} travel_state.lng_dir - The longitudinal direction. -1 if traveling to lesser longitude (left), 1 if traveling to greater longitude (right).
* @property {?number} travel_state.slope - The slope of the line segment formed by the two points between which the agent is traveling at this time during its trip.
* @property {Array} travel_state.path - A sequence of LatLngs; the agent will move from one to the next, popping each one off after it arrives until the end of the street; or, until the travel_state is changed/reset.
* @property {?function} update_func - Function to be called on each update.
*/
Agent.initialize = function(lat_lng, options, agentmap) {
this.agentmap = agentmap,
this.place = null,
this.travel_state = {
traveling: false,
current_point: null,
goal_point: null,
lat_dir: null,
lng_dir: null,
slope: null,
path: [],
};
this.update_func = function() {};
L.CircleMarker.prototype.initialize.call(this, lat_lng, options);
}
/**
* Stop the agent from traveling, reset all the properties of its travel state.
* @private
*/
Agent.resetTravelState = function() {
for (let key in this.travel_state) {
this.travel_state[key] =
key === "traveling" ? false :
key === "path" ? [] :
null;
}
};
/**
* Start a trip along the path specified in the agent's travel_state.
* @private
*/
Agent.startTrip = function() {
if (this.travel_state.path.length > 0) {
this.travelTo(this.travel_state.path[0]);
}
else {
throw new Error("The travel state's path is empty! There's no path to take a trip along!");
}
};
/**
* Set the agent to travel to some point on the map.
* @private
*
* @param {LatLng} goal_point - The point to which the agent should travel.
*/
Agent.travelTo = function(goal_point) {
let state = this.travel_state;
state.traveling = true,
state.current_point = this.getLatLng(),
state.goal_point = L.latLng(goal_point),
//Negating so that neg result corresponds to the goal being rightward/above, pos result to it being leftward/below.
state.lat_dir = Math.sign(- (state.current_point.lat - state.goal_point.lat)),
state.lng_dir = Math.sign(- (state.current_point.lng - state.goal_point.lng)),
state.slope = Math.abs(((state.current_point.lat - state.goal_point.lat) / (state.current_point.lng - state.goal_point.lng)));
};
/**
* Given the agent's currently scheduled trips (its path), get the place from which a new trip should start (namely, the end of the current path).
* That is: If there's already a path in queue, start the new path from the end of the existing one.
* @private
*
* @returns {Place} - The place where a new trip should start.
*/
Agent.newTripStartPlace = function() {
if (this.travel_state.path.length === 0) {
start_place = this.place;
}
else {
start_place = this.travel_state.path[this.travel_state.path.length - 1].new_place;
}
return start_place;
}
/**
* Schedule the agent to travel to a point within the unit he is in.
* @private
*
* @param {LatLng} goal_lat_lng - LatLng coordinate object for a point in the same unit the agent is in.
*/
Agent.setTravelInUnit = function(goal_lat_lng, goal_place) {
let goal_point = L.A.pointToCoordinateArray(goal_lat_lng),
//Buffering so that points on the perimeter, like the door, are captured. Might be more
//efficient to generate the door so that it's slightly inside the area.
goal_polygon = buffer(this.agentmap.units.getLayer(goal_place.unit).toGeoJSON(), .001);
if (booleanPointInPolygon(goal_point, goal_polygon)) {
goal_lat_lng.new_place = goal_place;
this.travel_state.path.push(goal_lat_lng);
}
else {
throw new Error("The goal_lat_lng is not inside of the polygon of the goal_place!");
}
};
/**
* Schedule the agent to travel directly from any point (e.g. of a street or unit) to a point (e.g. of another street or unit).
*
* @param {LatLng} goal_lat_lng - The point within the place to which the agent is to travel.
* @param {Place} goal_place - The place to which the agent will travel. Must be of form {"unit": unit_id} or {"street": street_id}.
* @param {Boolean} replace_trip - Whether to empty the currently scheduled path and replace it with this new trip; false by default (the new trip is
* simply appended to the current scheduled path).
*/
Agent.setTravelToPlace = function(goal_lat_lng, goal_place, replace_trip = false) {
let goal_layer = this.agentmap.units.getLayer(goal_place.unit) || this.agentmap.streets.getLayer(goal_place.street);
if (goal_layer) {
let goal_coords = L.A.pointToCoordinateArray(goal_lat_lng);
//Buffering so that points on the perimeter, like the door, are captured. Might be more
//efficient to generate the door so that it's slightly inside the area.
let goal_polygon = buffer(goal_layer.toGeoJSON(), .001);
if (booleanPointInPolygon(goal_coords, goal_polygon)) {
if (replace_trip === true) {
this.travel_state.path.length = 0;
}
let start_place = this.newTripStartPlace();
if (start_place.unit === goal_place.unit) {
this.setTravelInUnit(goal_lat_lng, goal_place);
return;
}
//Move to the street if it's starting at a unit and its goal is elsewhere.
else if (typeof(start_place.unit) === "number") {
let start_unit_door = this.agentmap.getUnitDoor(start_place.unit);
start_unit_door.new_place = start_place;
this.travel_state.path.push(start_unit_door);
let start_unit_street_id = this.agentmap.units.getLayer(start_place.unit).street_id,
start_unit_street_point = this.agentmap.getStreetNearDoor(start_place.unit);
start_unit_street_point.new_place = { street: start_unit_street_id };
this.travel_state.path.push(start_unit_street_point);
}
if (typeof(goal_place.unit) === "number") {
let goal_street_point = this.agentmap.getStreetNearDoor(goal_place.unit),
goal_street_point_place = { street: this.agentmap.units.getLayer(goal_place.unit).street_id };
//Move to the point on the street closest to the goal unit...
this.setTravelAlongStreet(goal_street_point, goal_street_point_place);
//Move from that point into the unit.
let goal_door = this.agentmap.getUnitDoor(goal_place.unit);
goal_door.new_place = goal_place;
this.travel_state.path.push(goal_door)
this.setTravelInUnit(goal_lat_lng, goal_place);
}
else if (typeof(goal_place.street) === "number") {
this.setTravelAlongStreet(goal_lat_lng, goal_place);
}
}
else {
throw new Error("The goal_lat_lng is not inside of the polygon of the goal_place!");
}
}
else {
throw new Error("No place exists matching the specified goal_place!");
}
};
/**
* Schedule the agent to travel to a point along the streets, via streets.
* @private
*
* @param {LatLng} goal_lat_lng - The coordinates of a point on a street to which the agent should travel.
* @param {Place} goal_place - The place to which the agent will travel. Must be of form {"street": street_id}.
*/
Agent.setTravelAlongStreet = function(goal_lat_lng, goal_place) {
let goal_coords,
goal_street_id,
goal_street_point,
goal_street_feature,
start_place = this.newTripStartPlace(),
start_street_id,
start_street_point,
start_street_feature;
if (typeof(start_place.street) === "number" &amp;&amp; typeof(goal_place.street) === "number") {
start_street_id = start_place.street,
start_street_point = this.travel_state.path.length !== 0 ?
this.travel_state.path[this.travel_state.path.length - 1] :
this.getLatLng();
start_street_point.new_place = {street: start_street_id};
goal_street_id = goal_place.street,
goal_street_feature = this.agentmap.streets.getLayer(goal_street_id).feature,
goal_coords = L.A.pointToCoordinateArray(goal_lat_lng),
goal_street_point = L.latLng(nearestPointOnLine(goal_street_feature, goal_coords).geometry.coordinates.reverse());
goal_street_point.new_place = goal_place;
}
else {
throw new Error("Both the start and end places must be streets!");
}
if (start_street_id === goal_street_id) {
this.setTravelOnSameStreet(start_street_point, goal_street_point, goal_street_feature, goal_street_id);
}
//If the start and end points are on different streets, move from the start to its nearest intersection, then from there
//to the intersection nearest to the end, and finally to the end.
else {
let start_nearest_intersection = this.agentmap.getNearestIntersection(start_street_point, start_place),
goal_nearest_intersection = this.agentmap.getNearestIntersection(goal_street_point, goal_place);
start_street_feature = this.agentmap.streets.getLayer(start_street_id).feature;
this.setTravelOnStreetNetwork(start_street_point, goal_street_point, start_nearest_intersection, goal_nearest_intersection);
}
};
/**
* Schedule the agent to travel between two points on the same street.
* @private
*
* @param start_lat_lng {LatLng} - The coordinates of the point on the street from which the agent will be traveling.
* @param goal_lat_lng {LatLng} - The coordinates of the point on the street to which the agent should travel.
* @param street_feature {Feature} - A GeoJSON object representing an OpenStreetMap street.
* @param street_id {number} - The ID of the street in the streets layerGroup.
*/
Agent.setTravelOnSameStreet = function(start_lat_lng, goal_lat_lng, street_feature, street_id) {
//lineSlice, regardless of the specified starting point, will give a segment with the same coordinate order
//as the original lineString array. So, if the goal point comes earlier in the array (e.g. it's on the far left),
//it'll end up being the first point in the path, instead of the last, and the agent will move to it directly,
//ignoring the street, and then travel along the street from the goal point to its original point (backwards).
//To fix this, I'm reversing the order of the coordinates in the segment if the last point in the line is closer
//to the agent's starting point than the first point on the line (implying it's a situation of the kind described above).
let start_coords = L.A.pointToCoordinateArray(start_lat_lng),
goal_coords = L.A.pointToCoordinateArray(goal_lat_lng),
street_path_unordered = L.A.reversedCoordinates(lineSlice(start_coords, goal_coords, street_feature).geometry.coordinates);
let start_to_path_beginning = start_lat_lng.distanceTo(L.latLng(street_path_unordered[0])),
start_to_path_end = start_lat_lng.distanceTo(L.latLng(street_path_unordered[street_path_unordered.length - 1]));
let street_path = start_to_path_beginning &lt; start_to_path_end ? street_path_unordered : street_path_unordered.reverse();
let street_path_lat_lngs = street_path.map(coords => {
let lat_lng = L.latLng(coords);
lat_lng.new_place = { street: street_id };
return lat_lng;
});
this.travel_state.path.push(...street_path_lat_lngs);
}
/**
* Schedule the agent up to travel between two points on a street network.
* @private
*
* @param start_lat_lng {LatLng} - The coordinates of the point on the street from which the agent will be traveling.
* @param goal_lat_lng {LatLng} - The coordinates of the point on the street to which the agent should travel.
* @param start_int_lat_lng {LatLng} - The coordinates of the nearest intersection on the same street at the start_lat_lng.
* @param goal_int_lat_lng {LatLng} - The coordinates of the nearest intersection on the same street as the goal_lat_lng.
*/
Agent.setTravelOnStreetNetwork = function(start_lat_lng, goal_lat_lng, start_int_lat_lng, goal_int_lat_lng) {
let path = this.agentmap.getPath(start_int_lat_lng, goal_int_lat_lng, start_lat_lng, goal_lat_lng, true);
for (let i = 0; i &lt;= path.length - 2; i++) {
let current_street_id = path[i].new_place.street,
current_street_feature = this.agentmap.streets.getLayer(current_street_id).feature;
this.setTravelOnSameStreet(path[i], path[i + 1], current_street_feature, current_street_id);
}
}
/**
* Continue to move the agent directly from one point to another, without regard for streets,
* according to the time that has passed since the last movement. Also simulate intermediary movements
* during the interval between the current call and the last call to moveDirectly, by splitting that interval
* up with some precision (agentmap.settings.movement_precision) into some number of parts (steps_inbetween)
* and moving slightly for each of them, for more precise collision detection than just doing it after each
* call to moveDirectly from requestAnimationFrame (max, 60 times per second) would allow. Limiting movements to
* each requestAnimationFrame call was causing each agent to skip too far ahead at each call, causing moveDirectly
* to not be able to catch when the agent is within 1 meter of the goal_point... splitting the interval since the last
* call up and making intermediary calls fixes that.
* @private
*
* @param {number} rAF_time - The time when the browser's most recent animation frame was released.
*/
Agent.moveDirectly = function(animation_interval, intermediary_interval, steps_inbetween) {
let state = this.travel_state;
//Fraction of the number of ticks since the last call to move the agent forward by.
//Only magnitudes smaller than hundredths will be added to the lat/lng at a time, so that it doesn't leap ahead too far;
//as the tick_interval is usually &lt; 1, and the magnitude will be the leap_fraction multiplied by the tick_interval.
const leap_fraction = .0001;
let move = (function(tick_interval) {
if (state.goal_point.distanceTo(state.current_point) &lt; 1) {
if (typeof(state.path[0].new_place) === "object") {
this.place = state.path[0].new_place;
}
state.path.shift();
if (state.path.length === 0) {
this.resetTravelState();
return;
}
else {
this.travelTo(state.path[0]);
}
}
let lat_change = state.lat_dir * state.slope * (leap_fraction * tick_interval),
lng_change = state.lng_dir * (leap_fraction * tick_interval),
new_lat_lng = L.latLng([state.current_point.lat + lat_change, state.current_point.lng + lng_change]);
this.setLatLng(new_lat_lng);
state.current_point = new_lat_lng;
}).bind(this);
//Intermediary movements.
for (let i = 0; i &lt; steps_inbetween; ++i) {
move(intermediary_interval);
if (state.traveling === false) {
return;
}
}
//Latest requested movement.
if (state.traveling === true) {
latest_interval = animation_interval - (this.agentmap.settings.movement_precision * steps_inbetween);
move(latest_interval);
}
else {
return;
}
};
/**
* Make the agent proceed with whatever it's doing and update its properties before the browser draws the next frame.
* @private
*
* @param {number} rAF_time - The time when the browser's most recent animation frame was released.
*/
Agent.update = function(animation_interval, intermediary_interval, steps_inbetween) {
this.update_func();
if (this.travel_state.traveling) {
this.moveDirectly(animation_interval, intermediary_interval, steps_inbetween);
}
}
/**
* Returns an agent object.
*
* @memberof Agentmap
*/
function agent(feature, options, agentmap) {
return new L.A.Agent(feature, options, agentmap);
}
Agentmap.prototype.agentify = agentify,
Agentmap.prototype.seqUnitAgentMaker = seqUnitAgentMaker;
exports.Agent = L.CircleMarker.extend(Agent),
exports.agent = agent;
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Agent.html">Agent</a></li><li><a href="Agentmap.html">Agentmap</a></li></ul><h3>Global</h3><ul><li><a href="global.html#agentify">agentify</a></li><li><a href="global.html#agentmap">agentmap</a></li><li><a href="global.html#buildingify">buildingify</a></li><li><a href="global.html#decodeCoordString">decodeCoordString</a></li><li><a href="global.html#encodeLatLng">encodeLatLng</a></li><li><a href="global.html#generateUnitFeatures">generateUnitFeatures</a></li><li><a href="global.html#getIntersections">getIntersections</a></li><li><a href="global.html#getPath">getPath</a></li><li><a href="global.html#getPathFinder">getPathFinder</a></li><li><a href="global.html#getStreetFeatures">getStreetFeatures</a></li><li><a href="global.html#getUnitAnchors">getUnitAnchors</a></li><li><a href="global.html#getUnitFeatures">getUnitFeatures</a></li><li><a href="global.html#isPointCoordinates">isPointCoordinates</a></li><li><a href="global.html#noOverlaps">noOverlaps</a></li><li><a href="global.html#pointToCoordinateArray">pointToCoordinateArray</a></li><li><a href="global.html#reversedCoordinates">reversedCoordinates</a></li><li><a href="global.html#streetsToGraph">streetsToGraph</a></li><li><a href="global.html#unitsOutOfStreets">unitsOutOfStreets</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Wed Aug 01 2018 23:53:46 GMT-0400 (Eastern Daylight Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>