From 91b1279a429a2c144871a3d279dc06638bd068da Mon Sep 17 00:00:00 2001 From: Andrew Gregory Tsesis Date: Tue, 17 Sep 2024 01:28:18 -0400 Subject: [PATCH] updated tooling --- tool/browserlessagentmap.js | 346 +++++++++-------- tool/browserlessbuildings.js | 727 ++++++++++++++++++++--------------- tool/browserlessrouting.js | 396 +++++++++++-------- tool/featuretool.js | 110 +++--- 4 files changed, 870 insertions(+), 709 deletions(-) diff --git a/tool/browserlessagentmap.js b/tool/browserlessagentmap.js index b26d5b1..b9b2da8 100644 --- a/tool/browserlessagentmap.js +++ b/tool/browserlessagentmap.js @@ -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); diff --git a/tool/browserlessbuildings.js b/tool/browserlessbuildings.js index 71ce9b3..9676710 100644 --- a/tool/browserlessbuildings.js +++ b/tool/browserlessbuildings.js @@ -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} - 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} - 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} - 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} 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 of pairs of points around which to anchor units along a street. + * @returns {Array>} - 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} - 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} 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; diff --git a/tool/browserlessrouting.js b/tool/browserlessrouting.js index 5e2e661..690ffca 100644 --- a/tool/browserlessrouting.js +++ b/tool/browserlessrouting.js @@ -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>} - 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.

* * 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} 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.

* * Typically happens when the goal point's nearest intersection is beyond it on the street, @@ -225,26 +267,44 @@ function checkStartExcess(path) { * @param {Array} 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; diff --git a/tool/featuretool.js b/tool/featuretool.js index dc75134..c83800f 100644 --- a/tool/featuretool.js +++ b/tool/featuretool.js @@ -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); + } }