updated tooling

This commit is contained in:
Andrew Gregory Tsesis 2024-09-17 01:28:18 -04:00
parent 0730fcce5a
commit 91b1279a42
4 changed files with 870 additions and 709 deletions

View File

@ -1,8 +1,8 @@
/* The Agentmap class, which turns a Leaflet map into a simulation platform. */
let L = require("./featuretool").L,
lineSlice = require('@turf/line-slice').default,
length = require('@turf/length').default;
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.
@ -22,22 +22,22 @@ length = require('@turf/length').default;
* @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);
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
(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);
};
/**
@ -45,13 +45,13 @@ Agentmap = function(map, animation_interval = 1) {
*
* @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);
Agentmap.prototype.setAnimationInterval = function (animation_interval) {
Agentmap.checkAnimIntervalOption(animation_interval);
this.animation_interval = animation_interval;
this.animation_interval = animation_interval;
this.agents.eachLayer(agent => agent.setLatLng(agent._latlng));
}
this.agents.eachLayer((agent) => agent.setLatLng(agent._latlng));
};
/**
* Check whether the animation interval option provided is valid.
@ -59,74 +59,71 @@ Agentmap.prototype.setAnimationInterval = function(animation_interval) {
*
* @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!");
}
}
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;
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);
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);
}
}
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;
}
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 the agentmap.
this.controller();
//Execute user-provided per-tick instructions for each agent.
this.agents.eachLayer(function(agent) {
agent.controller();
});
this.state.ticks += 1;
//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, 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;
Agentmap.prototype.pause = function () {
L.Util.cancelAnimFrame(this.state.animation_frame_id);
(this.state.running = false), (this.state.paused = true);
};
/**
@ -135,19 +132,19 @@ Agentmap.prototype.pause = function() {
* @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;
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;
};
/**
@ -156,125 +153,127 @@ Agentmap.prototype.getUnitDoor = function(unit_id) {
* @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;
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);
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!");
}
return point_in_depth;
}
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;
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!");
}
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 = [];
street_feature = this.streets.getLayer(street_id).toGeoJSON();
for (let intersection in intersections) {
for (let cross_point of intersections[intersection]) {
let intersection_point = cross_point[0],
distance = lat_lng.distanceTo(intersection_point);
let intersections = this.streets.getLayer(street_id).intersections,
intersection_points = [],
intersection_distances = [];
/* More precise, but slower, distance detection -- not necessary yet.
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;
}
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]);
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);
}
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.
@ -284,7 +283,7 @@ Agentmap.prototype.downloadUnits = function() {
* @returns {object} - An Agentmap instance.
*/
function agentmapFactory(map) {
return new Agentmap(map);
return new Agentmap(map);
}
/**
@ -293,10 +292,9 @@ function agentmapFactory(map) {
* @memberof L.LayerGroup
*/
function layerCount() {
return this.getLayers().length;
return this.getLayers().length;
}
L.LayerGroup.include({count: layerCount});
L.LayerGroup.include({ count: layerCount });
exports.Agentmap = Agentmap,
exports.agentmap = agentmapFactory;
(exports.Agentmap = Agentmap), (exports.agentmap = agentmapFactory);

View File

@ -3,17 +3,17 @@
/* Functions that help design and generate building units onto the map. */
let L = require("./featuretool").L,
new_status = require("./featuretool").new_status,
status_update = require("./featuretool").status_update,
end_status = require("./featuretool").end_status,
bearing = require('@turf/bearing').default,
destination = require('@turf/destination').default,
along = require('@turf/along').default,
lineIntersect = require('@turf/line-intersect').default,
intersect = require('@turf/intersect').default,
Agentmap = require('./browserlessagentmap').Agentmap,
streetsToGraph = require('./browserlessrouting').streetsToGraph,
getPathFinder = require('./browserlessrouting').getPathFinder;
new_status = require("./featuretool").new_status,
status_update = require("./featuretool").status_update,
end_status = require("./featuretool").end_status,
bearing = require("@turf/bearing").default,
destination = require("@turf/destination").default,
along = require("@turf/along").default,
lineIntersect = require("@turf/line-intersect").default,
intersect = require("@turf/intersect").default,
Agentmap = require("./browserlessagentmap").Agentmap,
streetsToGraph = require("./browserlessrouting").streetsToGraph,
getPathFinder = require("./browserlessrouting").getPathFinder;
/**
* Generate and setup the desired map features (e.g. streets, houses).
@ -31,11 +31,24 @@ getPathFinder = require('./browserlessrouting').getPathFinder;
* @param {object} [unit_layers] - If you want to load a previously generated AgentMaps.units object instead of generating one from scarch: A GeoJSON Feature Collection of an AgentMaps.units featureGroup.
* @param {object} [street_layers] - If you want to load a previously generated AgentMaps.streets object instead of generating one from scarch: A GeoJSON Feature Collection of an AgentMaps.streets featureGroup.
*/
function buildingify(bounding_box, OSM_data, street_options, unit_options, unit_layers, street_layers) {
setupStreetFeatures.call(this, OSM_data, street_options);
setupUnitFeatures.call(this, bounding_box, OSM_data, unit_options, unit_layers);
console.log("Finished!");
function buildingify(
bounding_box,
OSM_data,
street_options,
unit_options,
unit_layers,
street_layers,
) {
setupStreetFeatures.call(this, OSM_data, street_options);
setupUnitFeatures.call(
this,
bounding_box,
OSM_data,
unit_options,
unit_layers,
);
console.log("Finished!");
}
/**
@ -46,58 +59,55 @@ function buildingify(bounding_box, OSM_data, street_options, unit_options, unit_
* @param {object} [street_layers] - If you want to load a previously generated AgentMaps.streets object instead of generating one from scarch: A GeoJSON Feature Collection of an AgentMaps.streets featureGroup.
*/
function setupStreetFeatures(OSM_data, street_options, street_layers) {
console.log("Setting up streets...");
let default_options = {
"color": "yellow",
"weight": 4,
"opacity": .5
};
console.log("Setting up streets...");
let default_options = {
color: "yellow",
weight: 4,
opacity: 0.5,
};
street_options = Object.assign(default_options, street_options);
street_options = Object.assign(default_options, street_options);
let street_feature_collection;
let street_feature_collection;
if (typeof(street_layers) === "undefined") {
let street_features = getStreetFeatures(OSM_data);
street_feature_collection = {
type: "FeatureCollection",
features: street_features
};
}
else {
street_feature_collection = street_layers;
}
this.streets = L.geoJSON(
street_feature_collection,
street_options
);
//Map streets' OSM IDs to their Leaflet IDs.
this.streets.id_map = {};
if (typeof street_layers === "undefined") {
let street_features = getStreetFeatures(OSM_data);
//Having added the streets as layers to the map, do any processing that requires access to those layers.
new_status()
let i = 1;
this.streets.eachLayer(function(street) {
status_update("Relating " + i + " of " + this.streets.count() + " streets...");
this.streets.id_map[street.feature.id] = street._leaflet_id;
street_feature_collection = {
type: "FeatureCollection",
features: street_features,
};
} else {
street_feature_collection = street_layers;
}
addStreetLayerIntersections.call(this, street);
this.streets = L.geoJSON(street_feature_collection, street_options);
i++;
}, this);
end_status();
//Map streets' OSM IDs to their Leaflet IDs.
this.streets.id_map = {};
//Add general graph-making and path-finder-making methods to Agentmap, in case streets are added, removed, or modified mid-simulation.
console.log("Generating street graph...");
this.streetsToGraph = streetsToGraph,
this.getPathFinder = getPathFinder;
this.streets.graph = streetsToGraph(this.streets),
this.pathfinder = getPathFinder(this.streets.graph);
//Having added the streets as layers to the map, do any processing that requires access to those layers.
new_status();
let i = 1;
this.streets.eachLayer(function (street) {
status_update(
"Relating " + i + " of " + this.streets.count() + " streets...",
);
this.streets.id_map[street.feature.id] = street._leaflet_id;
addStreetLayerIntersections.call(this, street);
i++;
}, this);
end_status();
//Add general graph-making and path-finder-making methods to Agentmap, in case streets are added, removed, or modified mid-simulation.
console.log("Generating street graph...");
(this.streetsToGraph = streetsToGraph), (this.getPathFinder = getPathFinder);
(this.streets.graph = streetsToGraph(this.streets)),
(this.pathfinder = getPathFinder(this.streets.graph));
}
/**
@ -108,51 +118,64 @@ function setupStreetFeatures(OSM_data, street_options, street_layers) {
* @returns {Array<Feature>} - array of street features.
*/
function getStreetFeatures(OSM_data) {
let street_features = [];
let street_features = [];
for (let i = 0; i < OSM_data.features.length; ++i) {
let feature = OSM_data.features[i];
for (let i = 0; i < OSM_data.features.length; ++i) {
let feature = OSM_data.features[i];
if (feature.geometry.type === "LineString" && feature.properties.highway) {
let street_feature = feature;
if (feature.geometry.type === "LineString" && feature.properties.highway) {
let street_feature = feature;
street_features.push(street_feature);
}
}
street_features.push(street_feature);
}
}
return street_features;
return street_features;
}
/**
* Gets the intersections of all the streets on the map and adds them as properties to the street layers.
* @private
*
*
* @param {object} street - A Leaflet polyline representing a street.
*/
function addStreetLayerIntersections(street) {
let street_id = street._leaflet_id;
let street_id = street._leaflet_id;
street.intersections = typeof(street.intersections) === "undefined" ? {} : street.intersections;
street.intersections =
typeof street.intersections === "undefined" ? {} : street.intersections;
this.streets.eachLayer(function(other_street) {
let other_street_id = other_street._leaflet_id;
this.streets.eachLayer(function (other_street) {
let other_street_id = other_street._leaflet_id;
//Skip if both streets are the same, or if the street already has its intersections with the other street.
if (typeof(street.intersections[other_street_id]) === "undefined" && street_id !== other_street_id) {
let street_coords = street.getLatLngs().map(L.A.pointToCoordinateArray),
other_street_coords = other_street.getLatLngs().map(L.A.pointToCoordinateArray),
identified_intersections = L.A.getIntersections(street_coords, other_street_coords, [street_id, other_street_id]).map(
identified_intersection =>
[L.latLng(L.A.reversedCoordinates(identified_intersection[0])), identified_intersection[1]]
);
//Skip if both streets are the same, or if the street already has its intersections with the other street.
if (
typeof street.intersections[other_street_id] === "undefined" &&
street_id !== other_street_id
) {
let street_coords = street.getLatLngs().map(L.A.pointToCoordinateArray),
other_street_coords = other_street
.getLatLngs()
.map(L.A.pointToCoordinateArray),
identified_intersections = L.A.getIntersections(
street_coords,
other_street_coords,
[street_id, other_street_id],
).map((identified_intersection) => [
L.latLng(L.A.reversedCoordinates(identified_intersection[0])),
identified_intersection[1],
]);
if (identified_intersections.length > 0) {
street.intersections[other_street_id] = identified_intersections,
other_street.intersections = typeof(other_street.intersections) === "undefined" ? {} : other_street.intersections,
other_street.intersections[street_id] = identified_intersections;
}
}
});
if (identified_intersections.length > 0) {
(street.intersections[other_street_id] = identified_intersections),
(other_street.intersections =
typeof other_street.intersections === "undefined"
? {}
: other_street.intersections),
(other_street.intersections[street_id] = identified_intersections);
}
}
});
}
/**
@ -163,63 +186,76 @@ function addStreetLayerIntersections(street) {
* @param {object} unit_options - An object containing the Leaflet & AgentMaps styling options for units.
* @param {object} [unit_layers] - If you want to load a previously generated AgentMaps.units object instead of generating one from scarch: A GeoJSON Feature Collection of an AgentMaps.units featureGroup.
*/
function setupUnitFeatures(bounding_box, OSM_data, unit_options = {}, unit_layers) {
console.log("Setting up units...");
function setupUnitFeatures(
bounding_box,
OSM_data,
unit_options = {},
unit_layers,
) {
console.log("Setting up units...");
let default_options = {
"color": "green",
"weight": 1,
"opacity": .87,
"front_buffer": 6,
"side_buffer": 3,
"length": 14,
"depth": 18
};
let default_options = {
color: "green",
weight: 1,
opacity: 0.87,
front_buffer: 6,
side_buffer: 3,
length: 14,
depth: 18,
};
unit_options = Object.assign(default_options, unit_options);
let unit_feature_collection;
unit_options = Object.assign(default_options, unit_options);
//If no unit_layers is supplied, generate the units from scratch.
if (typeof(unit_layers) === "undefined") {
//Bind getUnitFeatures to "this" so it can access the agentmap as "this.agentmap".
let unit_features = getUnitFeatures.bind(this)(bounding_box, OSM_data, unit_options);
let unit_feature_collection;
unit_feature_collection = {
type: "FeatureCollection",
features: unit_features
};
}
else {
unit_feature_collection = unit_layers;
}
console.log("Loading units...");
this.units = L.geoJSON(
unit_feature_collection,
unit_options
);
new_status();
let i = 1;
//Having added the units as layers to the map, do any processing that requires access to those layers.
this.units.eachLayer(function(unit) {
if (typeof(unit_layers) === "undefined") {
unit.street_id = unit.feature.properties.street_id;
}
else {
unit.street_id = this.streets.id_map[unit.feature.properties.OSM_street_id];
}
//If no unit_layers is supplied, generate the units from scratch.
if (typeof unit_layers === "undefined") {
//Bind getUnitFeatures to "this" so it can access the agentmap as "this.agentmap".
let unit_features = getUnitFeatures.bind(this)(
bounding_box,
OSM_data,
unit_options,
);
unit.street_anchors = unit.feature.properties.street_anchors,
status_update("Finding neighbors for unit " + i + " of " + this.units.count() + " units.");
//Change the IDs of each unit in this unit's neighbours array into the appropriate Leaflet IDs.
unit.neighbors = getUnitNeighborLayerIDs.call(this, unit.feature.properties.neighbors);
i++;
}, this);
unit_feature_collection = {
type: "FeatureCollection",
features: unit_features,
};
} else {
unit_feature_collection = unit_layers;
}
end_status();
console.log("Loading units...");
this.units = L.geoJSON(unit_feature_collection, unit_options);
new_status();
let i = 1;
//Having added the units as layers to the map, do any processing that requires access to those layers.
this.units.eachLayer(function (unit) {
if (typeof unit_layers === "undefined") {
unit.street_id = unit.feature.properties.street_id;
} else {
unit.street_id =
this.streets.id_map[unit.feature.properties.OSM_street_id];
}
(unit.street_anchors = unit.feature.properties.street_anchors),
status_update(
"Finding neighbors for unit " +
i +
" of " +
this.units.count() +
" units.",
);
//Change the IDs of each unit in this unit's neighbours array into the appropriate Leaflet IDs.
unit.neighbors = getUnitNeighborLayerIDs.call(
this,
unit.feature.properties.neighbors,
);
i++;
}, this);
end_status();
}
/**
@ -231,24 +267,23 @@ function setupUnitFeatures(bounding_box, OSM_data, unit_options = {}, unit_layer
* @returns {Array<?number>} - An array of Leaflet layer IDs corresponding to the unit's neighbors.
*/
function getUnitNeighborLayerIDs(neighbors) {
let neighbor_layer_ids = neighbors.map(function(neighbor) {
if (neighbor !== null) {
let neighbor_layer_id = null;
this.units.eachLayer(function(possible_neighbor_layer) {
if (possible_neighbor_layer.feature.properties.id === neighbor) {
neighbor_layer_id = this.units.getLayerId(possible_neighbor_layer);
}
}, this);
let neighbor_layer_ids = neighbors.map(function (neighbor) {
if (neighbor !== null) {
let neighbor_layer_id = null;
return neighbor_layer_id;
}
else {
return null;
}
}, this);
this.units.eachLayer(function (possible_neighbor_layer) {
if (possible_neighbor_layer.feature.properties.id === neighbor) {
neighbor_layer_id = this.units.getLayerId(possible_neighbor_layer);
}
}, this);
return neighbor_layer_ids;
return neighbor_layer_id;
} else {
return null;
}
}, this);
return neighbor_layer_ids;
}
/**
@ -261,32 +296,48 @@ function getUnitNeighborLayerIDs(neighbors) {
* @returns {Array<Feature>} - array of features representing real estate units.
*/
function getUnitFeatures(bounding_box, OSM_data, unit_options) {
let proposed_unit_features = [];
new_status();
let i = 1;
this.streets.eachLayer(function(layer) {
status_update("Generating units for street " + i + " of " + this.streets.count() + " streets...");
let street_feature = layer.feature,
street_id = layer._leaflet_id,
street_OSM_id = layer.feature.id,
proposed_anchors = getUnitAnchors(street_feature, bounding_box, unit_options),
new_proposed_unit_features = generateUnitFeatures(proposed_anchors, proposed_unit_features, street_id, street_OSM_id, unit_options);
proposed_unit_features.push(...new_proposed_unit_features);
i++;
}, this);
let proposed_unit_features = [];
end_status();
unit_features = unitsOutOfStreets(proposed_unit_features, this.streets);
return unit_features;
new_status();
let i = 1;
this.streets.eachLayer(function (layer) {
status_update(
"Generating units for street " +
i +
" of " +
this.streets.count() +
" streets...",
);
let street_feature = layer.feature,
street_id = layer._leaflet_id,
street_OSM_id = layer.feature.id,
proposed_anchors = getUnitAnchors(
street_feature,
bounding_box,
unit_options,
),
new_proposed_unit_features = generateUnitFeatures(
proposed_anchors,
proposed_unit_features,
street_id,
street_OSM_id,
unit_options,
);
proposed_unit_features.push(...new_proposed_unit_features);
i++;
}, this);
end_status();
unit_features = unitsOutOfStreets(proposed_unit_features, this.streets);
return unit_features;
}
/**
* Given an array of anchor pairs, for each anchor pair find four
* Given an array of anchor pairs, for each anchor pair find four
* nearby points on either side of the street appropriate to build a unit(s) on.
* @private
*
@ -297,126 +348,162 @@ function getUnitFeatures(bounding_box, OSM_data, unit_options) {
* @param {object} unit_options - An object containing the AgentMaps styling options for units.
* @returns {Array<Feature>} unit_features - Array of features representing units.
*/
function generateUnitFeatures(unit_anchors, proposed_unit_features, street_leaflet_id, street_OSM_id, unit_options) {
//One sub-array of unit features for each side of the road.
let unit_features = [[],[]],
starting_id = proposed_unit_features.length,
increment = 1;
for (let anchor_pair of unit_anchors) {
//Pair of unit_features opposite each other on a street.
let unit_pair = [null, null];
for (let i of [1, -1]) {
let anchor_a = anchor_pair[0].geometry.coordinates,
anchor_b = anchor_pair[1].geometry.coordinates,
anchor_latLng_pair = [anchor_a, anchor_b],
street_buffer = unit_options.front_buffer / 1000, //Distance between center of street and start of unit.
house_depth = unit_options.depth / 1000,
angle = bearing(anchor_a, anchor_b),
new_angle = angle + i * 90, //Angle of line perpendicular to the anchor segment.
unit_feature = {
type: "Feature",
properties: {
street: "none"
},
geometry: {
type: "Polygon",
coordinates: [[]]
}
};
unit_feature.geometry.coordinates[0][0] = destination(anchor_a, street_buffer, new_angle).geometry.coordinates,
unit_feature.geometry.coordinates[0][1] = destination(anchor_b, street_buffer, new_angle).geometry.coordinates,
unit_feature.geometry.coordinates[0][2] = destination(anchor_b, street_buffer + house_depth, new_angle).geometry.coordinates,
unit_feature.geometry.coordinates[0][3] = destination(anchor_a, street_buffer + house_depth, new_angle).geometry.coordinates;
unit_feature.geometry.coordinates[0][4] = unit_feature.geometry.coordinates[0][0];
function generateUnitFeatures(
unit_anchors,
proposed_unit_features,
street_leaflet_id,
street_OSM_id,
unit_options,
) {
//One sub-array of unit features for each side of the road.
let unit_features = [[], []],
starting_id = proposed_unit_features.length,
increment = 1;
//Exclude the unit if it overlaps with any of the other proposed units.
let all_proposed_unit_features = unit_features[0].concat(unit_features[1]).concat(proposed_unit_features);
if (noOverlaps(unit_feature, all_proposed_unit_features)) {
//Recode index so that it's useful here.
i = i === 1 ? 0 : 1;
for (let anchor_pair of unit_anchors) {
//Pair of unit_features opposite each other on a street.
let unit_pair = [null, null];
unit_feature.properties.street_id = street_leaflet_id,
unit_feature.properties.OSM_street_id = street_OSM_id,
unit_feature.properties.street_anchors = anchor_latLng_pair,
unit_feature.properties.neighbors = [null, null, null],
unit_feature.properties.id = starting_id + increment,
increment += 1;
if (unit_features[i].length !== 0) {
//Make previous unit_feature this unit_feature's first neighbor.
unit_feature.properties.neighbors[0] = unit_features[i][unit_features[i].length - 1].properties.id,
//Make this unit_feature the previous unit_feature's second neighbor.
unit_features[i][unit_features[i].length - 1].properties.neighbors[1] = unit_feature.properties.id;
}
if (i === 0) {
unit_pair[0] = unit_feature;
}
else {
if (unit_pair[0] !== null) {
//Make unit_feature opposite to this unit_feature on the street its third neighbor.
unit_feature.properties.neighbors[2] = unit_pair[0].properties.id,
//Make unit_feature opposite to this unit_feature on the street's third neighbor this unit_feature.
unit_pair[0].properties.neighbors[2] = unit_feature.properties.id;
}
unit_pair[1] = unit_feature;
}
}
}
if (unit_pair[0] !== null) {
unit_features[0].push(unit_pair[0]);
}
for (let i of [1, -1]) {
let anchor_a = anchor_pair[0].geometry.coordinates,
anchor_b = anchor_pair[1].geometry.coordinates,
anchor_latLng_pair = [anchor_a, anchor_b],
street_buffer = unit_options.front_buffer / 1000, //Distance between center of street and start of unit.
house_depth = unit_options.depth / 1000,
angle = bearing(anchor_a, anchor_b),
new_angle = angle + i * 90, //Angle of line perpendicular to the anchor segment.
unit_feature = {
type: "Feature",
properties: {
street: "none",
},
geometry: {
type: "Polygon",
coordinates: [[]],
},
};
(unit_feature.geometry.coordinates[0][0] = destination(
anchor_a,
street_buffer,
new_angle,
).geometry.coordinates),
(unit_feature.geometry.coordinates[0][1] = destination(
anchor_b,
street_buffer,
new_angle,
).geometry.coordinates),
(unit_feature.geometry.coordinates[0][2] = destination(
anchor_b,
street_buffer + house_depth,
new_angle,
).geometry.coordinates),
(unit_feature.geometry.coordinates[0][3] = destination(
anchor_a,
street_buffer + house_depth,
new_angle,
).geometry.coordinates);
unit_feature.geometry.coordinates[0][4] =
unit_feature.geometry.coordinates[0][0];
if (unit_pair[1] !== null) {
unit_features[1].push(unit_pair[1]);
}
}
//Exclude the unit if it overlaps with any of the other proposed units.
let all_proposed_unit_features = unit_features[0]
.concat(unit_features[1])
.concat(proposed_unit_features);
if (noOverlaps(unit_feature, all_proposed_unit_features)) {
//Recode index so that it's useful here.
i = i === 1 ? 0 : 1;
let unit_features_merged = [].concat(...unit_features);
return unit_features_merged;
(unit_feature.properties.street_id = street_leaflet_id),
(unit_feature.properties.OSM_street_id = street_OSM_id),
(unit_feature.properties.street_anchors = anchor_latLng_pair),
(unit_feature.properties.neighbors = [null, null, null]),
(unit_feature.properties.id = starting_id + increment),
(increment += 1);
if (unit_features[i].length !== 0) {
//Make previous unit_feature this unit_feature's first neighbor.
(unit_feature.properties.neighbors[0] =
unit_features[i][unit_features[i].length - 1].properties.id),
//Make this unit_feature the previous unit_feature's second neighbor.
(unit_features[i][
unit_features[i].length - 1
].properties.neighbors[1] = unit_feature.properties.id);
}
if (i === 0) {
unit_pair[0] = unit_feature;
} else {
if (unit_pair[0] !== null) {
//Make unit_feature opposite to this unit_feature on the street its third neighbor.
(unit_feature.properties.neighbors[2] = unit_pair[0].properties.id),
//Make unit_feature opposite to this unit_feature on the street's third neighbor this unit_feature.
(unit_pair[0].properties.neighbors[2] =
unit_feature.properties.id);
}
unit_pair[1] = unit_feature;
}
}
}
if (unit_pair[0] !== null) {
unit_features[0].push(unit_pair[0]);
}
if (unit_pair[1] !== null) {
unit_features[1].push(unit_pair[1]);
}
}
let unit_features_merged = [].concat(...unit_features);
return unit_features_merged;
}
/**
* Find anchors for potential units. chors are the pairs of start
* Find anchors for potential units. chors are the pairs of start
* and end points along the street from which units may be constructed.
* @private
*
*
* @param {Feature} street_feature - A GeoJSON feature object representing a street.
* @param {object} unit_options - An object containing the AgentMaps styling options for units.
* @returns {Array<Array<Feature>>} - Array of pairs of points around which to anchor units along a street.
* @returns {Array<Array<Feature>>} - Array of pairs of points around which to anchor units along a street.
*/
function getUnitAnchors(street_feature, bounding_box, unit_options) {
let unit_anchors = [],
unit_length = unit_options.length / 1000, //Kilometers.
unit_buffer = unit_options.side_buffer / 1000, //Distance between units, kilometers.
endpoint = street_feature.geometry.coordinates[street_feature.geometry.coordinates.length - 1],
start_anchor = along(street_feature, 0),
end_anchor = along(street_feature, unit_length),
distance_along = unit_length;
while (end_anchor.geometry.coordinates != endpoint) {
//Exclude proposed anchors if they're outside of the bounding box.
start_coord = L.A.reversedCoordinates(start_anchor.geometry.coordinates),
end_coord = L.A.reversedCoordinates(end_anchor.geometry.coordinates);
if (L.latLngBounds(bounding_box).contains(start_coord) &&
L.latLngBounds(bounding_box).contains(end_coord)) {
unit_anchors.push([start_anchor, end_anchor]);
}
let unit_anchors = [],
unit_length = unit_options.length / 1000, //Kilometers.
unit_buffer = unit_options.side_buffer / 1000, //Distance between units, kilometers.
endpoint =
street_feature.geometry.coordinates[
street_feature.geometry.coordinates.length - 1
],
start_anchor = along(street_feature, 0),
end_anchor = along(street_feature, unit_length),
distance_along = unit_length;
//Find next pair of anchors.
start_anchor = along(street_feature, distance_along + unit_buffer);
end_anchor = along(street_feature, distance_along + unit_buffer + unit_length);
distance_along += unit_buffer + unit_length;
}
while (end_anchor.geometry.coordinates != endpoint) {
//Exclude proposed anchors if they're outside of the bounding box.
(start_coord = L.A.reversedCoordinates(start_anchor.geometry.coordinates)),
(end_coord = L.A.reversedCoordinates(end_anchor.geometry.coordinates));
return unit_anchors;
if (
L.latLngBounds(bounding_box).contains(start_coord) &&
L.latLngBounds(bounding_box).contains(end_coord)
) {
unit_anchors.push([start_anchor, end_anchor]);
}
//Find next pair of anchors.
start_anchor = along(street_feature, distance_along + unit_buffer);
end_anchor = along(
street_feature,
distance_along + unit_buffer + unit_length,
);
distance_along += unit_buffer + unit_length;
}
return unit_anchors;
}
/**
@ -428,28 +515,41 @@ function getUnitAnchors(street_feature, bounding_box, unit_options) {
* @returns {Array<Feature>} - unit_features, but with all units that intersect any streets removed.
*/
function unitsOutOfStreets(unit_features, street_layers) {
let processed_unit_features = unit_features.slice();
new_status();
let i = 1;
street_layers.eachLayer(function(street_layer) {
status_update("Removing superfluous units from streets. Checking street " + i + " of " + street_layers.count() + "...");
let street_feature = street_layer.feature;
for (let unit_feature of processed_unit_features) {
let intersection_exists = lineIntersect(street_feature, unit_feature).features.length > 0;
if (intersection_exists) {
processed_unit_features.splice(processed_unit_features.indexOf(unit_feature), 1, null);
}
}
let processed_unit_features = unit_features.slice();
processed_unit_features = processed_unit_features.filter(feature => feature === null ? false : true);
i++;
});
end_status();
new_status();
let i = 1;
street_layers.eachLayer(function (street_layer) {
status_update(
"Removing superfluous units from streets. Checking street " +
i +
" of " +
street_layers.count() +
"...",
);
return processed_unit_features;
let street_feature = street_layer.feature;
for (let unit_feature of processed_unit_features) {
let intersection_exists =
lineIntersect(street_feature, unit_feature).features.length > 0;
if (intersection_exists) {
processed_unit_features.splice(
processed_unit_features.indexOf(unit_feature),
1,
null,
);
}
}
processed_unit_features = processed_unit_features.filter((feature) =>
feature === null ? false : true,
);
i++;
});
end_status();
return processed_unit_features;
}
/**
@ -459,16 +559,19 @@ function unitsOutOfStreets(unit_features, street_layers) {
* @param {Feature} reference_polygon_feature - A geoJSON polygon feature.
* @param {Array<Feature>} polygon_feature_array - Array of geoJSON polygon features.
* @returns {boolean} - Whether the polygon_feature overlaps with any one in the array.
*/
*/
function noOverlaps(reference_polygon_feature, polygon_feature_array) {
for (feature_array_element of polygon_feature_array) {
let overlap_exists = intersect(reference_polygon_feature, feature_array_element);
if (overlap_exists) {
return false;
}
}
for (feature_array_element of polygon_feature_array) {
let overlap_exists = intersect(
reference_polygon_feature,
feature_array_element,
);
if (overlap_exists) {
return false;
}
}
return true;
return true;
}
Agentmap.prototype.buildingify = buildingify;

View File

@ -3,29 +3,29 @@
/* Here we have utilities to convert OSM geojson data into a distance-weighted graph and find the shortest path between two points. */
let L = require("./featuretool").L,
path = require("ngraph.path"),
createGraph = require("ngraph.graph"),
lineSlice = require('@turf/line-slice').default,
length = require('@turf/length').default,
Agentmap = require('./browserlessagentmap').Agentmap;
path = require("ngraph.path"),
createGraph = require("ngraph.graph"),
lineSlice = require("@turf/line-slice").default,
length = require("@turf/length").default,
Agentmap = require("./browserlessagentmap").Agentmap;
/**
* Convert a layerGroup of streets into a graph. Useful if you modify the street layers during the simulation
* and want routing to work with the new street network.
*
* @param {LayerGroup} streets - A Leaflet layerGroup of streets, forming a street network.
* @returns {Object} - A graph representing the street network, operable by the ngraph pathfinder.
* @returns {Object} - A graph representing the street network, operable by the ngraph pathfinder.
*/
function streetsToGraph(streets) {
let graph = createGraph(),
streetToGraphBound = streetToGraph.bind(this, graph);
//For each street, get an array of indices for the start, intersections, and end coordinates, in order from
//start to end. Then, add the coordinates at each index as a node, and an edge between each adjacent node in the array,
//associating the distance between the nodes (between their coordinates) with each edge.
streets.eachLayer(streetToGraphBound);
let graph = createGraph(),
streetToGraphBound = streetToGraph.bind(this, graph);
return graph;
//For each street, get an array of indices for the start, intersections, and end coordinates, in order from
//start to end. Then, add the coordinates at each index as a node, and an edge between each adjacent node in the array,
//associating the distance between the nodes (between their coordinates) with each edge.
streets.eachLayer(streetToGraphBound);
return graph;
}
/**
@ -35,71 +35,81 @@ function streetsToGraph(streets) {
* @param {L.Polyline} street - A Leaflet Polyline layer for a street.
*/
function streetToGraph(graph, street) {
let street_id = street._leaflet_id,
intersection_indices = [],
street_points = street.getLatLngs();
//Populate intersection_indices with the indices of all of the street's intersections in its coordinate array.
for (let cross_street in street.intersections) {
let intersections = street.intersections[cross_street];
for (let intersection of intersections) {
let intersection_index = intersection[1][street_id];
//Ignore duplicate intersection points (caused by 3-way intersections).
if (!intersection_indices.some(other_intersection_index => other_intersection_index === intersection_index)) {
intersection_indices.push(intersection_index);
}
}
}
let street_id = street._leaflet_id,
intersection_indices = [],
street_points = street.getLatLngs();
//Sort the intersection_indices so that they are in order from the start of the street's coordinate array to the end;
//this is why we're not getting the raw coordinates, but their indices first, so they can be sorted.
intersection_indices = intersection_indices.sort(function(a, b) {
return a - b;
});
//Populate intersection_indices with the indices of all of the street's intersections in its coordinate array.
for (let cross_street in street.intersections) {
let intersections = street.intersections[cross_street];
//Check if beginning and end points of the street are in the intersection_incides; if not, add them.
if (!intersection_indices.some(intersection_index => intersection_index === 0)) {
intersection_indices.unshift(0);
}
if (!intersection_indices.some(intersection_index => intersection_index === street_points.length - 1)) {
intersection_indices.push(street_points.length - 1);
}
for (let intersection of intersections) {
let intersection_index = intersection[1][street_id];
//Make a graph out of segments of the street between the start, intersections, and end of the street,
//so that the nodes are the coordinates of the start, end, and intersection points, and the edges are
//the segments between successive nodes. Each edge is associated with the geographic distance between its nodes.
for (let i = 0; i <= intersection_indices.length - 2; i++) {
let node_a = street_points[intersection_indices[i]],
node_b = street_points[intersection_indices[i + 1]],
a_string = encodeLatLng(node_a),
b_string = encodeLatLng(node_b),
start_coords = L.A.pointToCoordinateArray(node_a),
end_coords = L.A.pointToCoordinateArray(node_b),
segment = lineSlice(start_coords, end_coords, street.toGeoJSON()),
distance = length(segment);
graph.addLink(a_string, b_string, {
distance: distance,
place: { type: "street",
id: street_id }
});
}
//Ignore duplicate intersection points (caused by 3-way intersections).
if (
!intersection_indices.some(
(other_intersection_index) =>
other_intersection_index === intersection_index,
)
) {
intersection_indices.push(intersection_index);
}
}
}
//Sort the intersection_indices so that they are in order from the start of the street's coordinate array to the end;
//this is why we're not getting the raw coordinates, but their indices first, so they can be sorted.
intersection_indices = intersection_indices.sort(function (a, b) {
return a - b;
});
//Check if beginning and end points of the street are in the intersection_incides; if not, add them.
if (
!intersection_indices.some((intersection_index) => intersection_index === 0)
) {
intersection_indices.unshift(0);
}
if (
!intersection_indices.some(
(intersection_index) => intersection_index === street_points.length - 1,
)
) {
intersection_indices.push(street_points.length - 1);
}
//Make a graph out of segments of the street between the start, intersections, and end of the street,
//so that the nodes are the coordinates of the start, end, and intersection points, and the edges are
//the segments between successive nodes. Each edge is associated with the geographic distance between its nodes.
for (let i = 0; i <= intersection_indices.length - 2; i++) {
let node_a = street_points[intersection_indices[i]],
node_b = street_points[intersection_indices[i + 1]],
a_string = encodeLatLng(node_a),
b_string = encodeLatLng(node_b),
start_coords = L.A.pointToCoordinateArray(node_a),
end_coords = L.A.pointToCoordinateArray(node_b),
segment = lineSlice(start_coords, end_coords, street.toGeoJSON()),
distance = length(segment);
graph.addLink(a_string, b_string, {
distance: distance,
place: { type: "street", id: street_id },
});
}
}
/**
* Given a street network (graph), return a pathfinder that can operate on it.
* Useful if you modify the street graph during the simulation.
*
*
* @param {object} graph - An ngraph graph representing an OSM street network.
* @returns {object} - An A* pathfinder for the graph.
*/
function getPathFinder(graph) {
return path.aStar(graph, {
distance(fromNode, toNode, link) {
return link.data.distance;
}
});
return path.aStar(graph, {
distance(fromNode, toNode, link) {
return link.data.distance;
},
});
}
/**
@ -115,71 +125,92 @@ function getPathFinder(graph) {
* @param {Boolean} [sparse=false] - Whether to exclude intersections between the first and last along a street-specific path (which are superfluous for extracting the necessary sub-street).
* @return {Array<Array<number>>} - An array of points along the graph, leading from the start to the end.
*/
function getPath(start_int_lat_lng, goal_int_lat_lng, start_lat_lng, goal_lat_lng, sparse = false) {
let start_coord = encodeLatLng(start_int_lat_lng),
end_coord = encodeLatLng(goal_int_lat_lng),
encoded_path = this.pathfinder.find(start_coord, end_coord),
path = [];
if (encoded_path.length > 0 && decodeCoordString(encoded_path[0].id).distanceTo(start_int_lat_lng) >
decodeCoordString(encoded_path[0].id).distanceTo(goal_int_lat_lng)) {
encoded_path = encoded_path.reverse();
}
function getPath(
start_int_lat_lng,
goal_int_lat_lng,
start_lat_lng,
goal_lat_lng,
sparse = false,
) {
let start_coord = encodeLatLng(start_int_lat_lng),
end_coord = encodeLatLng(goal_int_lat_lng),
encoded_path = this.pathfinder.find(start_coord, end_coord),
path = [];
if (sparse === true && encoded_path.length >= 2) {
let sparse_path = [],
recent_street = null,
current_street = null;
for (let i = 0; i <= encoded_path.length - 2; i++) {
current_street = this.streets.graph.getLink(encoded_path[i].id, encoded_path[i + 1].id) ||
this.streets.graph.getLink(encoded_path[i + 1].id, encoded_path[i].id);
if (recent_street === null || current_street.data.place.id !== recent_street.data.place.id) {
let decoded_coords = decodeCoordString(encoded_path[i].id, current_street.data.place);
sparse_path.push(decoded_coords);
}
//If the last place on the path to the goal is labeled with a different street id than the goal,
//add it to the sparse path.
if (i === encoded_path.length - 2) {
let decoded_coords = decodeCoordString(encoded_path[i + 1].id, current_street.data.place);
sparse_path.push(decoded_coords);
}
}
path = sparse_path;
}
else {
path = encoded_path.map(point => decodeCoordString(point.id, 0));
}
path.unshift(start_lat_lng);
path.push(goal_lat_lng);
//If the goal point lies before the first intersection of the goal street, then the 2nd to last point in the
//path will have the previous street's id attached to it. If the goal lies on a different street, make
//sure the 2nd to last point (the street path intersection point before the goal) has the same street id as the goal.
if (path[path.length - 2].new_place.id !== goal_lat_lng.new_place.id) {
path[path.length - 2].new_place = goal_lat_lng.new_place;
}
if (
encoded_path.length > 0 &&
decodeCoordString(encoded_path[0].id).distanceTo(start_int_lat_lng) >
decodeCoordString(encoded_path[0].id).distanceTo(goal_int_lat_lng)
) {
encoded_path = encoded_path.reverse();
}
//If the second [to last] point--namely the intersection closest to the start [goal]--is further from the third
//[to last] point than the goal, and all three points are on the same street, remove the second [to last] point.
if (path.length >= 3) {
checkStartExcess.call(this, path);
checkEndExcess.call(this, path);
}
return path;
if (sparse === true && encoded_path.length >= 2) {
let sparse_path = [],
recent_street = null,
current_street = null;
for (let i = 0; i <= encoded_path.length - 2; i++) {
current_street =
this.streets.graph.getLink(
encoded_path[i].id,
encoded_path[i + 1].id,
) ||
this.streets.graph.getLink(encoded_path[i + 1].id, encoded_path[i].id);
if (
recent_street === null ||
current_street.data.place.id !== recent_street.data.place.id
) {
let decoded_coords = decodeCoordString(
encoded_path[i].id,
current_street.data.place,
);
sparse_path.push(decoded_coords);
}
//If the last place on the path to the goal is labeled with a different street id than the goal,
//add it to the sparse path.
if (i === encoded_path.length - 2) {
let decoded_coords = decodeCoordString(
encoded_path[i + 1].id,
current_street.data.place,
);
sparse_path.push(decoded_coords);
}
}
path = sparse_path;
} else {
path = encoded_path.map((point) => decodeCoordString(point.id, 0));
}
path.unshift(start_lat_lng);
path.push(goal_lat_lng);
//If the goal point lies before the first intersection of the goal street, then the 2nd to last point in the
//path will have the previous street's id attached to it. If the goal lies on a different street, make
//sure the 2nd to last point (the street path intersection point before the goal) has the same street id as the goal.
if (path[path.length - 2].new_place.id !== goal_lat_lng.new_place.id) {
path[path.length - 2].new_place = goal_lat_lng.new_place;
}
//If the second [to last] point--namely the intersection closest to the start [goal]--is further from the third
//[to last] point than the goal, and all three points are on the same street, remove the second [to last] point.
if (path.length >= 3) {
checkStartExcess.call(this, path);
checkEndExcess.call(this, path);
}
return path;
}
//checkStartExcess and checkEndExcess are _much_ easier to follow given distinct variable names,
//and so they are not abstracted into one more general function.
/**
/**
* If the first two points after the start point share the same street as the start point, and the
* third point is closer to the first (start) point than it is to the second point, remove the
* third point is closer to the first (start) point than it is to the second point, remove the
* second point, as it's a superfluous detour.<br/><br/>
*
* Typically happens when the start point's nearest intersection is beyond it on the street,
@ -190,30 +221,41 @@ function getPath(start_int_lat_lng, goal_int_lat_lng, start_lat_lng, goal_lat_ln
* @param {Array<LatLng>} path - An array of LatLngs representing a path for an agent to travel along.
*/
function checkStartExcess(path) {
let start_street = this.streets.getLayer(path[0].new_place.id),
second_street_id = path[1].new_place.id,
start_second_intersections = start_street.intersections[second_street_id],
second_is_intersection = typeof(start_second_intersections) === "undefined" ? false :
start_second_intersections.some(intersection =>
intersection[0].lat === path[1].lat && intersection[0].lng === path[1].lng),
third_street_id = path[2].new_place.id,
start_third_intersections = start_street.intersections[third_street_id],
third_is_intersection = typeof(start_third_intersections) === "undefined" ? false :
start_third_intersections.some(intersection =>
intersection[0].lat === path[2].lat && intersection[0].lng === path[2].lng);
let start_street = this.streets.getLayer(path[0].new_place.id),
second_street_id = path[1].new_place.id,
start_second_intersections = start_street.intersections[second_street_id],
second_is_intersection =
typeof start_second_intersections === "undefined"
? false
: start_second_intersections.some(
(intersection) =>
intersection[0].lat === path[1].lat &&
intersection[0].lng === path[1].lng,
),
third_street_id = path[2].new_place.id,
start_third_intersections = start_street.intersections[third_street_id],
third_is_intersection =
typeof start_third_intersections === "undefined"
? false
: start_third_intersections.some(
(intersection) =>
intersection[0].lat === path[2].lat &&
intersection[0].lng === path[2].lng,
);
if ((second_is_intersection || second_street_id === path[0].new_place.id) &&
(third_is_intersection || third_street_id === path[0].new_place.id)) {
if (path[2].distanceTo(path[0]) <
path[2].distanceTo(path[1])) {
path.splice(1, 1);
}
}
if (
(second_is_intersection || second_street_id === path[0].new_place.id) &&
(third_is_intersection || third_street_id === path[0].new_place.id)
) {
if (path[2].distanceTo(path[0]) < path[2].distanceTo(path[1])) {
path.splice(1, 1);
}
}
}
/**
/**
* If the last two points before the goal point share the same street as the goal point, and the
* first point is closer to the third (goal) point than it is to the second point, remove the
* first point is closer to the third (goal) point than it is to the second point, remove the
* second point, as it's a superfluous detour.<br/><br/>
*
* Typically happens when the goal point's nearest intersection is beyond it on the street,
@ -225,26 +267,44 @@ function checkStartExcess(path) {
* @param {Array<LatLng>} path - An array of LatLngs representing a path for an agent to travel along.
*/
function checkEndExcess(path) {
let goal_street = this.streets.getLayer(path[path.length - 1].new_place.id),
second_to_last_street_id = path[path.length - 2].new_place.id,
goal_second_to_last_intersections = goal_street.intersections[second_to_last_street_id],
second_to_last_is_intersection = typeof(goal_second_to_last_intersections) === "undefined" ? false :
goal_second_to_last_intersections.some(intersection =>
intersection[0].lat === path[path.length - 1].lat && intersection[0].lng === path[path.length - 1].lng),
third_last_street_id = path[path.length - 3].new_place.id,
goal_third_last_intersections = goal_street.intersections[third_last_street_id],
third_last_is_intersection = typeof(goal_third_last_intersections) === "undefined" ? false :
goal_third_last_intersections.some(intersection =>
intersection[0].lat === path[path.length - 3].lat && intersection[0].lng === path[path.length - 3].lng);
let goal_street = this.streets.getLayer(path[path.length - 1].new_place.id),
second_to_last_street_id = path[path.length - 2].new_place.id,
goal_second_to_last_intersections =
goal_street.intersections[second_to_last_street_id],
second_to_last_is_intersection =
typeof goal_second_to_last_intersections === "undefined"
? false
: goal_second_to_last_intersections.some(
(intersection) =>
intersection[0].lat === path[path.length - 1].lat &&
intersection[0].lng === path[path.length - 1].lng,
),
third_last_street_id = path[path.length - 3].new_place.id,
goal_third_last_intersections =
goal_street.intersections[third_last_street_id],
third_last_is_intersection =
typeof goal_third_last_intersections === "undefined"
? false
: goal_third_last_intersections.some(
(intersection) =>
intersection[0].lat === path[path.length - 3].lat &&
intersection[0].lng === path[path.length - 3].lng,
);
if ((second_to_last_is_intersection || second_to_last_street_id === path[path.length - 1].new_place.id) &&
(third_last_is_intersection || third_last_street_id === path[path.length - 1].new_place.id) &&
path.length >= 3) {
if (path[path.length - 3].distanceTo(path[path.length - 1]) <
path[path.length - 3].distanceTo(path[path.length - 2])) {
path.splice(path.length - 2, 1);
}
}
if (
(second_to_last_is_intersection ||
second_to_last_street_id === path[path.length - 1].new_place.id) &&
(third_last_is_intersection ||
third_last_street_id === path[path.length - 1].new_place.id) &&
path.length >= 3
) {
if (
path[path.length - 3].distanceTo(path[path.length - 1]) <
path[path.length - 3].distanceTo(path[path.length - 2])
) {
path.splice(path.length - 2, 1);
}
}
}
/**
@ -255,7 +315,7 @@ function checkEndExcess(path) {
* @returns {string} - A string containing coordinates in the format of "Latitude,Longitude".
*/
function encodeLatLng(lat_lng) {
return lat_lng.lat.toString() + "," + lat_lng.lng.toString();
return lat_lng.lat.toString() + "," + lat_lng.lng.toString();
}
/**
@ -267,11 +327,11 @@ function encodeLatLng(lat_lng) {
* @returns {LatLng} - The coordinates encoded by the coord_string.
*/
function decodeCoordString(coord_string, place) {
let coord_strings = coord_string.split(","),
lat_lng = L.latLng(coord_strings);
lat_lng.new_place = place;
let coord_strings = coord_string.split(","),
lat_lng = L.latLng(coord_strings);
lat_lng.new_place = place;
return lat_lng;
return lat_lng;
}
Agentmap.prototype.getPath = getPath;

View File

@ -5,28 +5,28 @@
/* Core of a command line tool that runs buildingify and exports the resulting street and unit layers. */
let optimist = require("optimist"),
fs = require("fs"),
path = require("path");
fs = require("fs"),
path = require("path");
//Mock a browser environment.
var window = {
screen: {}
},
document = {
documentElement: {
style: []
},
createElement: () => ({
getContext: null
}),
},
navigator = {
userAgent: {
toLowerCase: () => ""
},
platform: ""
},
requestAnimationFrame = () => null;
screen: {},
},
document = {
documentElement: {
style: [],
},
createElement: () => ({
getContext: null,
}),
},
navigator = {
userAgent: {
toLowerCase: () => "",
},
platform: "",
},
requestAnimationFrame = () => null;
//Setup the Leaflet namespace.
let L = require("leaflet");
@ -34,17 +34,17 @@ L.A = require(path.join("..", "src", "utils"));
//Logging functions.
function new_status(text = "") {
process.stdout.write(text);
process.stdout.write(text);
}
function status_update(text = "") {
process.stdout.clearLine();
process.stdout.write(text);
process.stdout.cursorTo(0);
process.stdout.clearLine();
process.stdout.write(text);
process.stdout.cursorTo(0);
}
function end_status() {
process.stdout.write("\n");
process.stdout.write("\n");
}
//Let other modules access L and the logging functions.
@ -55,8 +55,8 @@ exports.end_status = end_status;
//Mock an AgentMaps object.
function AgentMock() {
this.streets = null;
this.units = null;
this.streets = null;
this.units = null;
}
let browserlessbuildings = require("./browserlessbuildings");
@ -67,47 +67,47 @@ let agentmock = new AgentMock();
//Accept as input an array specifying the bounding box and
//the name of a file containing the GeoJSON of streets wihin it.
let bounding_box = JSON.parse(optimist.argv["bbox"]),
OSM_file = path.normalize(optimist.argv["streets"]);
OSM_file = path.normalize(optimist.argv["streets"]);
fs.readFile(OSM_file, "utf8", function(error, data) {
readError(error);
processFile(data)
fs.readFile(OSM_file, "utf8", function (error, data) {
readError(error);
processFile(data);
});
//Given the bounding box and OSM data, extract the necessary info,
//generate the buildings (streets and units), and save them as files.
function processFile(data) {
let start = data.indexOf("{");
data = data.slice(start);
let start = data.indexOf("{");
data = data.slice(start);
if (data[data.length - 1] !== "}") {
data = data.slice(0, -1);
}
let OSM_data = JSON.parse(data);
agentmock.buildingify(bounding_box, OSM_data);
let streets = agentmock.streets.toGeoJSON(20),
units = agentmock.units.toGeoJSON(20);
if (data[data.length - 1] !== "}") {
data = data.slice(0, -1);
}
let streets_contents = "var streets_data = " + JSON.stringify(streets) + ";",
units_contents = "var units_data = " + JSON.stringify(units) + ";";
fs.writeFile("street_features.js", streets_contents, writeError);
fs.writeFile("unit_features.js", units_contents, writeError);
let OSM_data = JSON.parse(data);
agentmock.buildingify(bounding_box, OSM_data);
let streets = agentmock.streets.toGeoJSON(20),
units = agentmock.units.toGeoJSON(20);
let streets_contents = "var streets_data = " + JSON.stringify(streets) + ";",
units_contents = "var units_data = " + JSON.stringify(units) + ";";
fs.writeFile("street_features.js", streets_contents, writeError);
fs.writeFile("unit_features.js", units_contents, writeError);
}
function writeError(error) {
if (error) {
let prefix = "There was an issue saving your data: ";
return console.log(prefix + error);
}
if (error) {
let prefix = "There was an issue saving your data: ";
return console.log(prefix + error);
}
}
function readError(error) {
if (error) {
let prefix = "There was an issue accessing your data: ";
return console.log(prefix + error);
}
if (error) {
let prefix = "There was an issue accessing your data: ";
return console.log(prefix + error);
}
}