mirror of
https://github.com/noncomputable/AgentMaps.git
synced 2026-01-18 16:27:05 +00:00
831 lines
32 KiB
HTML
831 lines
32 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>/* Here we define agentify, the agent base class, and everything they uniquely rely on. */
|
|
|
|
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,
|
|
length = require('@turf/length').default,
|
|
lineString = require('@turf/helpers').lineString,
|
|
bearing = require('@turf/bearing').default,
|
|
destination = require('@turf/destination').default,
|
|
Agentmap = require('./agentmap').Agentmap,
|
|
encodeLatLng = require('./routing').encodeLatLng;
|
|
|
|
/**
|
|
* The main class representing individual agents, using Leaflet class system.
|
|
* @private
|
|
*
|
|
* @class Agent
|
|
*/
|
|
let Agent = {};
|
|
|
|
/**
|
|
* 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 specifying where the agent is currently at.
|
|
* @property {number} [steps_made=0] - The number of steps the agent has moved since the beginning.
|
|
* @property {Object} this.trip - Properties detailing information about the agent's trip that change sometimes, but needs to be accessed by future updates.
|
|
* @property {boolean} this.trip.moving - Whether the agent currently moving.
|
|
* @property {boolean} this.trip.paused - Whether the agent should be allowed to move along its trip.
|
|
* @property {?Point} this.trip.current_point - The point where the agent is currently located.
|
|
* @property {?Point} this.trip.goal_point - The point where the agent is traveling to.
|
|
* @property {?number} this.trip.lat_dir - The latitudinal direction. -1 if traveling to lower latitude (down), 1 if traveling to higher latitude (up).
|
|
* @property {?number} this.trip.lng_dir - The longitudinal direction. -1 if traveling to lesser longitude (left), 1 if traveling to greater longitude (right).
|
|
* @property {?number} this.trip.speed - The speed that the agent should travel, in meters per tick.
|
|
* @property {?number} this.trip.angle - The angle between the current point and the goal.
|
|
* @property {?number} this.trip.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} this.trip.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 trip is changed/reset.
|
|
* @property {?function} controller - User-defined function to be called on each update (each tick).
|
|
* @property {?function} fine_controller - User-defined function to be called before & after each movemnt (on each step an agent performs during a tick).
|
|
*/
|
|
Agent.initialize = function(lat_lng, options, agentmap) {
|
|
this.agentmap = agentmap,
|
|
this.place = null,
|
|
this.steps_made = 0,
|
|
this.trip = {
|
|
paused: false,
|
|
moving: false,
|
|
current_point: null,
|
|
goal_point: null,
|
|
lat_dir: null,
|
|
lng_dir: null,
|
|
slope: null,
|
|
angle: null,
|
|
speed: null,
|
|
path: [],
|
|
},
|
|
this.controller = function() {},
|
|
this.fine_controller = function() {};
|
|
|
|
L.CircleMarker.prototype.initialize.call(this, lat_lng, options);
|
|
}
|
|
|
|
/**
|
|
* Reset all the properties of its trip, but don't change whether it's allowed to be traveling or not.
|
|
* @memberof Agent
|
|
* @instance
|
|
*/
|
|
Agent.resetTrip = function() {
|
|
for (let key in this.trip) {
|
|
this.trip[key] =
|
|
key === "paused" ? false :
|
|
key === "moving" ? false :
|
|
key === "path" ? [] :
|
|
null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Set the agent up to start traveling along the path specified in the agent's trip..
|
|
* @memberof Agent
|
|
* @instance
|
|
*/
|
|
Agent.startTrip = function() {
|
|
if (this.trip.path.length > 0) {
|
|
this.travelTo(this.trip.path[0]);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Stop the agent where it is along its trip.
|
|
* @memberof Agent
|
|
* @instance
|
|
*/
|
|
Agent.pauseTrip = function() {
|
|
this.trip.paused = true;
|
|
};
|
|
|
|
/**
|
|
* Have the agent continue from where it was left off along its trip.
|
|
* @memberof Agent
|
|
* @instance
|
|
*/
|
|
Agent.resumeTrip = function() {
|
|
this.trip.paused = false;
|
|
};
|
|
|
|
/**
|
|
* Set the agent to travel to some point on the map.
|
|
* @memberof Agent
|
|
* @instance
|
|
* @private
|
|
*
|
|
* @param {LatLng} goal_point - The point to which the agent should travel.
|
|
*/
|
|
Agent.travelTo = function(goal_point) {
|
|
this.trip.current_point = this.getLatLng(),
|
|
this.trip.goal_point = goal_point,
|
|
|
|
//Negating so that neg result corresponds to the goal being rightward/above, pos result to it being leftward/below.
|
|
this.trip.lat_dir = Math.sign(- (this.trip.current_point.lat - this.trip.goal_point.lat)),
|
|
this.trip.lng_dir = Math.sign(- (this.trip.current_point.lng - this.trip.goal_point.lng)),
|
|
|
|
this.trip.angle = bearing(L.A.pointToCoordinateArray(this.trip.current_point), L.A.pointToCoordinateArray(this.trip.goal_point));
|
|
this.trip.slope = Math.abs((this.trip.current_point.lat - this.trip.goal_point.lat) / (this.trip.current_point.lng - this.trip.goal_point.lng));
|
|
this.trip.speed = this.trip.goal_point.speed;
|
|
|
|
//If the agent won't be at any particular place at least until it reaches its next goal, mark its place as unanchored.
|
|
if (this.trip.path[0].new_place.type === "unanchored" || this.trip.path[0].move_directly === true) {
|
|
this.place = {type: "unanchored"};
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Given the agent's currently scheduledthis.trips (its path), get the place from which a newthis.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.
|
|
* @memberof Agent
|
|
* @instance
|
|
* @private
|
|
*
|
|
* @returns {Place} - The place where a newthis.trip should start.
|
|
*/
|
|
Agent.newTripStartPlace = function() {
|
|
if (this.trip.path.length === 0) {
|
|
start_place = this.place;
|
|
}
|
|
else {
|
|
start_place = this.trip.path[this.trip.path.length - 1].new_place;
|
|
}
|
|
|
|
return start_place;
|
|
}
|
|
|
|
/**
|
|
* Schedule the agent to travel to a point within the unit he is in.
|
|
* @memberof Agent
|
|
* @instance
|
|
* @private
|
|
*
|
|
* @param {LatLng} goal_lat_lng - LatLng coordinate object for a point in the same unit the agent is in.
|
|
* @param {number} speed - The speed that the agent should travel, in meters per tick.
|
|
*/
|
|
Agent.setTravelInUnit = function(goal_lat_lng, goal_place, speed) {
|
|
goal_lat_lng.new_place = goal_place,
|
|
goal_lat_lng.speed = speed;
|
|
this.trip.path.push(goal_lat_lng);
|
|
};
|
|
|
|
/**
|
|
* 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).
|
|
* @memberof Agent
|
|
* @instance
|
|
*
|
|
* @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.
|
|
* @param {number} [speed=1] - The speed in meters per tick that the agent should try to travel. Must be >= .1.
|
|
* @param {Boolean} [move_directly=false] - Whether to ignore the streets & roads and move directly to the goal.
|
|
* @param {Boolean} [replace_trip=false] - 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, speed = 1, move_directly = false, replace_trip = false) {
|
|
this.checkSpeed(speed);
|
|
|
|
let start_place = this.newTripStartPlace();
|
|
goal_lat_lng = L.latLng(goal_lat_lng);
|
|
|
|
if (replace_trip === true) {
|
|
this.resetTrip();
|
|
}
|
|
|
|
//If either the agent is already unanchored or its goal is unanchored, just schedule it to move directly to its goal.
|
|
if (start_place.type === "unanchored" || goal_place.type === "unanchored" || move_directly === true) {
|
|
let goal = goal_lat_lng;
|
|
goal.new_place = goal_place,
|
|
goal.move_directly = true,
|
|
goal.speed = speed;
|
|
|
|
this.trip.path.push(goal);
|
|
|
|
return;
|
|
}
|
|
|
|
let goal_layer = this.agentmap.units.getLayer(goal_place.id) || this.agentmap.streets.getLayer(goal_place.id);
|
|
|
|
//If the goal isn't unanchored, see if it's a street or a unit and schedule the agent appropriately.
|
|
if (goal_layer) {
|
|
let goal_coords = L.A.pointToCoordinateArray(goal_lat_lng);
|
|
|
|
//Buffering so that points on the perimeter, like the door, are captured.
|
|
//Also expands street lines into thin polygons (booleanPointInPolygon requires polys).
|
|
//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 (start_place.type === "unit" && goal_place.type === "unit" && start_place.id === goal_place.id) {
|
|
this.setTravelInUnit(goal_lat_lng, goal_place, speed);
|
|
return;
|
|
}
|
|
//Move to the street if it's starting at a unit and its goal is elsewhere.
|
|
else if (start_place.type === "unit") {
|
|
let start_unit_door = this.agentmap.getUnitDoor(start_place.id);
|
|
start_unit_door.new_place = start_place,
|
|
start_unit_door.speed = speed;
|
|
this.trip.path.push(start_unit_door);
|
|
|
|
let start_unit_street_id = this.agentmap.units.getLayer(start_place.id).street_id,
|
|
start_unit_street_point = this.agentmap.getStreetNearDoor(start_place.id);
|
|
start_unit_street_point.new_place = { type: "street", id: start_unit_street_id },
|
|
start_unit_street_point.speed = speed;
|
|
this.trip.path.push(start_unit_street_point);
|
|
}
|
|
|
|
if (goal_place.type === "unit") {
|
|
let goal_street_point = this.agentmap.getStreetNearDoor(goal_place.id),
|
|
goal_street_point_place = { type: "street", id: this.agentmap.units.getLayer(goal_place.id).street_id };
|
|
|
|
//Move to the point on the street closest to the goal unit...
|
|
this.setTravelAlongStreet(goal_street_point, goal_street_point_place, speed);
|
|
|
|
//Move from that point into the unit.
|
|
let goal_door = this.agentmap.getUnitDoor(goal_place.id);
|
|
goal_door.new_place = goal_place,
|
|
goal_door.speed = speed;
|
|
this.trip.path.push(goal_door)
|
|
this.setTravelInUnit(goal_lat_lng, goal_place, speed);
|
|
}
|
|
else if (goal_place.street === "number") {
|
|
this.setTravelAlongStreet(goal_lat_lng, goal_place, speed);
|
|
}
|
|
}
|
|
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!");
|
|
}
|
|
};
|
|
|
|
Agent.scheduleTrip = Agent.setTravelToPlace;
|
|
|
|
/**
|
|
* Schedule the agent to travel to a point along the streets, via streets.
|
|
* @memberof Agent
|
|
* @instance
|
|
* @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 a street.
|
|
* @param {number} speed - The speed that the agent should travel, in meters per tick.
|
|
*/
|
|
Agent.setTravelAlongStreet = function(goal_lat_lng, goal_place, speed) {
|
|
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 (start_place.type === "street" && goal_place.type === "street") {
|
|
start_street_id = start_place.id,
|
|
start_street_point = this.trip.path.length !== 0 ?
|
|
this.trip.path[this.trip.path.length - 1] :
|
|
this.getLatLng();
|
|
start_street_point.new_place = {type: "street", id: start_street_id};
|
|
|
|
goal_street_id = goal_place.id,
|
|
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, speed);
|
|
}
|
|
//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, speed);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Schedule the agent to travel between two points on the same street.
|
|
* @memberof Agent
|
|
* @instance
|
|
* @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.
|
|
* @param {number} speed - The speed that the agent should travel, in meters per tick.
|
|
*/
|
|
Agent.setTravelOnSameStreet = function(start_lat_lng, goal_lat_lng, street_feature, street_id, speed) {
|
|
//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 points that should come before it. It would 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 the last point in the array is the starting
|
|
//point, not the goal).
|
|
|
|
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 < 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 = { type: "street", id: street_id },
|
|
lat_lng.speed = speed;
|
|
|
|
return lat_lng;
|
|
});
|
|
|
|
let first_lat = street_path_lat_lngs[0].lat,
|
|
first_lng = street_path_lat_lngs[0].lng;
|
|
|
|
//Exclude the last point if it's the same as the second to last point of this proposed segment,
|
|
//and the second of it's the same as the first.
|
|
//(since lineSlice adds a point for each other street in an intersection).
|
|
if (street_path_lat_lngs.length > 1) {
|
|
let second_lat = street_path_lat_lngs[1].lat,
|
|
second_lng = street_path_lat_lngs[1].lng,
|
|
final_lat = street_path_lat_lngs[street_path_lat_lngs.length - 1].lat,
|
|
final_lng = street_path_lat_lngs[street_path_lat_lngs.length - 1].lng,
|
|
penultimate_lat = street_path_lat_lngs[street_path_lat_lngs.length - 2].lat,
|
|
penultimate_lng = street_path_lat_lngs[street_path_lat_lngs.length - 2].lng;
|
|
|
|
if (first_lat === second_lat && first_lng === second_lng) {
|
|
street_path_lat_lngs.shift();
|
|
}
|
|
|
|
if (final_lat === penultimate_lat && final_lng === penultimate_lng) {
|
|
street_path_lat_lngs.pop();
|
|
}
|
|
}
|
|
|
|
//Exclude the first point if it's already the last point of the already scheduled path.
|
|
if (this.trip.path.length > 0) {
|
|
let prev_lat = this.trip.path[this.trip.path.length - 1].lat,
|
|
prev_lng = this.trip.path[this.trip.path.length - 1].lng;
|
|
|
|
if (prev_lat === first_lat && prev_lng === first_lng) {
|
|
street_path_lat_lngs.shift();
|
|
}
|
|
}
|
|
|
|
this.trip.path.push(...street_path_lat_lngs);
|
|
}
|
|
|
|
/**
|
|
* Schedule the agent up to travel between two points on a street network.
|
|
* @memberof Agent
|
|
* @instance
|
|
* @private
|
|
*
|
|
* @param {LatLng} start_lat_lng - The coordinates of the point on the street from which the agent will be traveling.
|
|
* @param {LatLng} goal_lat_lng - The coordinates of the point on the street to which the agent should travel.
|
|
* @param {LatLng} start_int_lat_lng - The coordinates of the nearest intersection on the same street at the start_lat_lng.
|
|
* @param {LatLng} goal_int_lat_lng - The coordinates of the nearest intersection on the same street as the goal_lat_lng.
|
|
* @param {number} speed - The speed that the agent should travel, in meters per tick.
|
|
*/
|
|
Agent.setTravelOnStreetNetwork = function(start_lat_lng, goal_lat_lng, start_int_lat_lng, goal_int_lat_lng, speed) {
|
|
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 <= path.length - 2; i++) {
|
|
let current_street_id = path[i].new_place.id,
|
|
current_street_feature = this.agentmap.streets.getLayer(current_street_id).feature;
|
|
|
|
this.setTravelOnSameStreet(path[i], path[i + 1], current_street_feature, current_street_id, speed);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set a new, constant speed for the agent to move along its currently scheduled path.
|
|
* @memberof Agent
|
|
* @instance
|
|
*
|
|
* @param {number} speed - The speed (in meters per tick) that the agent should move. Must be >= .1.
|
|
*/
|
|
Agent.setSpeed = function(speed) {
|
|
this.checkSpeed(speed);
|
|
|
|
if (this.trip.goal_point !== null) {
|
|
this.trip.speed = speed;
|
|
}
|
|
|
|
for (let spot of this.trip.path) {
|
|
this.trip.speed = speed;
|
|
spot.speed = speed;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Multiply the speed the agent moves along its currently scheduled path by a constant.
|
|
* @memberof Agent
|
|
* @instance
|
|
*
|
|
* @param {number} multiplier - The number to multiply the agent's scheduled speed by.
|
|
* All scheduled speeds must be >= .1.
|
|
*/
|
|
Agent.multiplySpeed = function(multiplier) {
|
|
if (this.trip.goal_point !== null) {
|
|
this.trip.speed *= multiplier;
|
|
this.checkSpeed(this.trip.speed);
|
|
}
|
|
|
|
for (let spot of this.trip.path) {
|
|
spot.speed *= multiplier;
|
|
this.checkSpeed(spot.speed);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Increase the speed the agent moves along its currently scheduled path by a constant.
|
|
* @memberof Agent
|
|
* @instance
|
|
*
|
|
* @param {number} magnitude - The number to add to the agent's scheduled speed.
|
|
* All scheduled speeds must be >= .1
|
|
*/
|
|
Agent.increaseSpeed = function(magnitude) {
|
|
if (this.trip.goal_point !== null) {
|
|
this.trip.speed += magnitude;
|
|
this.checkSpeed(this.trip.speed);
|
|
}
|
|
|
|
for (let spot of this.trip.path) {
|
|
spot.speed += magnitude;
|
|
this.checkSpeed(spot.speed);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check whether a given speed is greater than the minimum.
|
|
* @memberof Agent
|
|
* @instance
|
|
*
|
|
* @param {number} speed - A number representing the speed of an agent in meters per second.
|
|
*/
|
|
Agent.checkSpeed = function(speed) {
|
|
if (speed < .1) {
|
|
throw new Error("Cannot assign speed below .1 to agent!");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Continue to move the agent directly along the points in its path, at approximately the speed associated with each point in the path.
|
|
* Since two points along the path may be far apart, the agent will make multiple intermediary movements too, splitting up its transfer
|
|
* from its current point to its goal point into a sub-path with multiple sub-goals.
|
|
* @memberof Agent
|
|
* @instance
|
|
* @private
|
|
*
|
|
* @param {number} override_speed - Have the agent step this distance, instead of the distance suggested by the current state's speed property.
|
|
*/
|
|
Agent.travel = function(override_speed) {
|
|
let current_coords = L.A.pointToCoordinateArray(this.trip.current_point),
|
|
sub_goal_distance = override_speed ||this.trip.speed,
|
|
sub_goal_coords = destination(current_coords, sub_goal_distance * .001,this.trip.angle).geometry.coordinates,
|
|
sub_goal_lat_lng = L.latLng(L.A.reversedCoordinates(sub_goal_coords));
|
|
|
|
let segment_to_goal = lineString([this.trip.current_point, this.trip.goal_point].map(point => L.A.pointToCoordinateArray(point))),
|
|
segment_to_sub_goal = lineString([this.trip.current_point, sub_goal_lat_lng].map(point => L.A.pointToCoordinateArray(point)));
|
|
|
|
let goal_lat_dist = Math.abs(this.trip.current_point.lat - this.trip.goal_point.lat),
|
|
goal_lng_dist = Math.abs(this.trip.current_point.lng - this.trip.goal_point.lng);
|
|
|
|
let dist_to_goal = length(segment_to_goal) * 1000,
|
|
dist_to_sub_goal = length(segment_to_sub_goal) * 1000,
|
|
leftover_after_goal;
|
|
|
|
//Check if the distance to the sub_goal is greater than the distance to the goal, and if so, make the sub_goal equal the goal
|
|
//and change the number of meters to the sub_goal to the number of meters to the goal.
|
|
if (dist_to_goal < dist_to_sub_goal) {
|
|
sub_goal_lat_lng = this.trip.goal_point,
|
|
sub_goal_distance = dist_to_goal,
|
|
leftover_after_goal = dist_to_sub_goal - dist_to_goal;
|
|
}
|
|
|
|
if (this.checkArrival(sub_goal_lat_lng, leftover_after_goal)) {
|
|
return;
|
|
}
|
|
|
|
//Lat/Lng distance between current point and sub_goal point.
|
|
let sub_goal_lat_dist = Math.abs(sub_goal_lat_lng.lat - this.trip.current_point.lat),
|
|
sub_goal_lng_dist = Math.abs(sub_goal_lat_lng.lng - this.trip.current_point.lng);
|
|
|
|
let half_meters = sub_goal_distance * 2,
|
|
int_half_meters = Math.floor(half_meters),
|
|
int_lat_step_value = this.trip.lat_dir * (sub_goal_lat_dist / half_meters),
|
|
int_lng_step_value = this.trip.lng_dir * (sub_goal_lng_dist / half_meters),
|
|
final_lat_step_value = this.trip.lat_dir * (sub_goal_lat_dist - Math.abs(int_lat_step_value * int_half_meters)),
|
|
final_lng_step_value = this.trip.lng_dir * (sub_goal_lng_dist - Math.abs(int_lng_step_value * int_half_meters));
|
|
|
|
//Intermediary movements.
|
|
for (let i = 0; i < int_half_meters; ++i) {
|
|
this.step(int_lat_step_value, int_lng_step_value);
|
|
|
|
//If the agent is moving directly from a large distance, redirect it back towards the goal if it appears off course.
|
|
if (this.trip.goal_point.move_directly === true) {
|
|
let new_goal_lat_dist = Math.abs(this.trip.current_point.lat - this.trip.goal_point.lat),
|
|
new_goal_lng_dist = Math.abs(this.trip.current_point.lng - this.trip.goal_point.lng);
|
|
|
|
if (new_goal_lat_dist > goal_lat_dist || new_goal_lng_dist > goal_lng_dist) {
|
|
this.travelTo(this.trip.goal_point);
|
|
}
|
|
}
|
|
|
|
if (this.checkArrival(sub_goal_lat_lng, leftover_after_goal)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
//Last movement after intermediary movements.
|
|
this.step(final_lat_step_value, final_lng_step_value, true);
|
|
|
|
if (this.checkArrival(sub_goal_lat_lng, leftover_after_goal)) {
|
|
return;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Move the agent a given latitude and longitude.
|
|
* @memberof Agent
|
|
* @instance
|
|
* @private
|
|
*
|
|
* @param {number} lat_step_value - The number to add to the agent's latitude.
|
|
* @param {number} lng_step_value - The number to add to the agent's longitude.
|
|
*/
|
|
Agent.step = function(lat_step_value, lng_step_value) {
|
|
let new_lat_lng = L.latLng([this.trip.current_point.lat + lat_step_value, this.trip.current_point.lng + lng_step_value]);
|
|
|
|
this.trip.current_point = new_lat_lng,
|
|
this.steps_made++;
|
|
|
|
//Only redraw the Agent's position if the number of steps the agent has moved is a multiple of the agentmap.animation_interval.
|
|
if (this.agentmap.animation_interval > 0 && this.steps_made % this.agentmap.animation_interval === 0) {
|
|
this.setLatLng(new_lat_lng);
|
|
}
|
|
else {
|
|
this._latlng = new_lat_lng;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Check if the agent has arrived at the next goal in its path or to a sub_goal along the way and perform appropriate arrival operations.
|
|
* @memberof Agent
|
|
* @instance
|
|
* @private
|
|
*
|
|
* @param {LatLng} sub_goal_lat_lng - A sub_goal on the way to the goal (possibly the goal itself).
|
|
* @param {number} leftover_after_goal - If the agent arrives at its goal during the tick, the number of meters, according to its speed,
|
|
* leftover beyond the goal that it should still move during the tick.
|
|
*/
|
|
Agent.checkArrival = function(sub_goal_lat_lng, leftover_after_goal) {
|
|
if (this.trip.goal_point.distanceTo(this.trip.current_point) < .1) {
|
|
this.place = this.trip.path[0].new_place;
|
|
arrived = true;
|
|
|
|
this.trip.path.shift();
|
|
|
|
if (this.trip.path.length === 0) {
|
|
this.resetTrip();
|
|
}
|
|
else {
|
|
this.travelTo(this.trip.path[0]);
|
|
|
|
//If it still needs to move a certain distance during this tick, move it that distance towards the next goal before returning.
|
|
if (leftover_after_goal > 0) {
|
|
this.travel(leftover_after_goal);
|
|
}
|
|
}
|
|
|
|
this.trip.moving = false;
|
|
|
|
return true;
|
|
}
|
|
else if (sub_goal_lat_lng.distanceTo(this.trip.current_point) < .1) {
|
|
this.trip.moving = false;
|
|
|
|
return true;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Make the agent proceed along its trip.
|
|
* @memberof Agent
|
|
* @instance
|
|
*/
|
|
Agent.moveIt = function() {
|
|
//Make sure the agent isn't paused or already moving.
|
|
if (!this.trip.paused && !this.trip.moving) {
|
|
//Call the agent's fine_controller before it begins moving.
|
|
this.fine_controller();
|
|
|
|
//Check if the agent has a goal point, and if so travel towards it.
|
|
if (this.trip.goal_point !== null) {
|
|
this.trip.moving = true;
|
|
this.travel();
|
|
}
|
|
//Otherwise, if there's a scheduled path that the agent hasn't started traveling on yet,
|
|
//start traveling on it.
|
|
else if (this.trip.path.length !== 0) {
|
|
this.trip.moving = true;
|
|
this.startTrip();
|
|
this.travel();
|
|
}
|
|
}
|
|
}
|
|
|
|
Agent = L.CircleMarker.extend(Agent);
|
|
|
|
/**
|
|
* Returns an agent object.
|
|
*
|
|
* @param {LatLng} lat_lng - A pair of coordinates to locate 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.
|
|
*/
|
|
function agent(lat_lng, options, agentmap) {
|
|
return new Agent(lat_lng, options, agentmap);
|
|
}
|
|
|
|
/**
|
|
* A user-defined callback function that returns a feature with appropriate geometry and properties to represent an agent.
|
|
*
|
|
* @callback agentFeatureMaker
|
|
* @param {number} id - The agent's Leaflet layer ID.
|
|
* @returns {Point} - a GeoJSON Point feature with properties and coordinates for the agent, including
|
|
* a "place" property that will set the agent's initial {@link Place} and an object "layer_options" property
|
|
* that will specify the feature's Leaflet options (like its color, size, etc.). All other provided properties
|
|
* will be transferred to the Agent object once it is created.
|
|
* See {@link https://leafletjs.com/reference-1.3.2.html#circlemarker} for all possible layer options.
|
|
*
|
|
* @example
|
|
* let point = {
|
|
* "type": "Feature",
|
|
* "properties": {
|
|
* "layer_options": {
|
|
* "color": "red",
|
|
* "radius": .5,
|
|
* },
|
|
* "place": {
|
|
* "type": "unit",
|
|
* "id": 89
|
|
* },
|
|
*
|
|
* age: 72,
|
|
* home_city: "LA"
|
|
* },
|
|
* "geometry" {
|
|
* "type": "Point",
|
|
* "coordinates": [
|
|
* 14.54589,
|
|
* 57.136239
|
|
* ]
|
|
* }
|
|
* }
|
|
*/
|
|
|
|
/**
|
|
* A standard {@link agentFeatureMaker}, which sets an agent's location to be the point near the center of the iᵗʰ unit of the map,
|
|
* its place property to be that unit's, and its layer_options to be red and of radius .5 meters.
|
|
* @memberof Agentmap
|
|
* @instance
|
|
* @type {agentFeatureMaker}
|
|
*/
|
|
function seqUnitAgentMaker(id){
|
|
let index = this.agents.count();
|
|
|
|
if (index > this.units.getLayers().length - 1) {
|
|
throw new Error("seqUnitAgentMaker cannot accommodate more agents than there are units.");
|
|
}
|
|
|
|
let unit = this.units.getLayers()[index],
|
|
unit_id = this.units.getLayerId(unit),
|
|
center_point = centroid(unit.feature);
|
|
center_point.properties.place = {"type": "unit", "id": 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.
|
|
* @memberof Agentmap
|
|
* @instance
|
|
*
|
|
* @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.featureGroup().addTo(this.map);
|
|
}
|
|
|
|
let agents_existing = agentmap.agents.getLayers().length;
|
|
for (let i = agents_existing; i < agents_existing + count; i++) {
|
|
let new_agent = agent(null, null, agentmap);
|
|
|
|
//Callback function aren't automatically bound to the agentmap.
|
|
let boundFeatureMaker = agentFeatureMaker.bind(agentmap),
|
|
agent_feature = boundFeatureMaker(new_agent._leaflet_id);
|
|
|
|
let coordinates = L.A.reversedCoordinates(agent_feature.geometry.coordinates),
|
|
place = agent_feature.properties.place,
|
|
layer_options = agent_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.id) !== "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.setLatLng(coordinates);
|
|
new_agent.setStyle(layer_options);
|
|
|
|
delete agent_feature.properties.layer_options;
|
|
Object.assign(new_agent, agent_feature.properties);
|
|
|
|
this.agents.addLayer(new_agent);
|
|
}
|
|
}
|
|
|
|
Agentmap.prototype.agent = agent,
|
|
Agentmap.prototype.agentify = agentify,
|
|
Agentmap.prototype.seqUnitAgentMaker = seqUnitAgentMaker;
|
|
|
|
exports.Agent = 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>Tutorials</h3><ul><li><a href="tutorial-quickstart.html">quickstart</a></li></ul><h3>Global</h3><ul><li><a href="global.html#addStreetLayerIntersections">addStreetLayerIntersections</a></li><li><a href="global.html#agent">agent</a></li><li><a href="global.html#agentmap">agentmap</a></li><li><a href="global.html#checkEndExcess">checkEndExcess</a></li><li><a href="global.html#checkStartExcess">checkStartExcess</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#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#getUnitNeighborLayerIDs">getUnitNeighborLayerIDs</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#setupStreetFeatures">setupStreetFeatures</a></li><li><a href="global.html#setupUnitFeatures">setupUnitFeatures</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 Fri Aug 31 2018 17:06:01 GMT-0400 (Eastern Daylight Time)
|
|
</footer>
|
|
|
|
<script> prettyPrint(); </script>
|
|
<script src="scripts/linenumber.js"> </script>
|
|
</body>
|
|
</html>
|