mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-12-25 07:25:47 +03:00
Plumb in the tuned routing params. When they're not the defaults, automatically fallback from CH to Dijkstra's. #494
This commit is contained in:
parent
eebe7a098a
commit
32bcfbe738
@ -49,7 +49,11 @@ impl ObjectDebugger {
|
||||
let mut costs = Vec::new();
|
||||
for turn in map.get_turns_to_lane(l.id) {
|
||||
costs.push(map_model::connectivity::driving_cost(
|
||||
l, turn, constraint, map,
|
||||
l,
|
||||
turn,
|
||||
constraint,
|
||||
map.routing_params(),
|
||||
map,
|
||||
));
|
||||
}
|
||||
println!("Costs for {:?}: {:?}", constraint, costs);
|
||||
|
@ -1,5 +1,5 @@
|
||||
use map_gui::ID;
|
||||
use map_model::NORMAL_LANE_THICKNESS;
|
||||
use map_model::{RoutingParams, NORMAL_LANE_THICKNESS};
|
||||
use sim::{TripEndpoint, TripMode};
|
||||
use widgetry::{
|
||||
Color, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Line, Outcome, Panel,
|
||||
@ -26,39 +26,35 @@ impl RouteExplorer {
|
||||
Line("Route explorer").small_heading().draw(ctx),
|
||||
ctx.style().btn_close_widget(ctx),
|
||||
]),
|
||||
profile_to_controls(ctx, &RoutingProfile::default_biking()).named("profile"),
|
||||
params_to_controls(ctx, TripMode::Bike, &RoutingParams::default()).named("params"),
|
||||
]))
|
||||
.aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
|
||||
.build(ctx),
|
||||
})
|
||||
}
|
||||
|
||||
fn controls_to_profile(&self) -> RoutingProfile {
|
||||
fn controls_to_params(&self) -> (TripMode, RoutingParams) {
|
||||
let mut params = RoutingParams::default();
|
||||
if !self.panel.is_button_enabled("cars") {
|
||||
return RoutingProfile::Driving;
|
||||
return (TripMode::Drive, params);
|
||||
}
|
||||
if !self.panel.is_button_enabled("pedestrians") {
|
||||
return RoutingProfile::Walking;
|
||||
}
|
||||
RoutingProfile::Biking {
|
||||
bike_lane_penalty: self.panel.spinner("bike lane penalty") as f64 / 10.0,
|
||||
bus_lane_penalty: self.panel.spinner("bus lane penalty") as f64 / 10.0,
|
||||
driving_lane_penalty: self.panel.spinner("driving lane penalty") as f64 / 10.0,
|
||||
return (TripMode::Walk, params);
|
||||
}
|
||||
params.bike_lane_penalty = self.panel.spinner("bike lane penalty") as f64 / 10.0;
|
||||
params.bus_lane_penalty = self.panel.spinner("bus lane penalty") as f64 / 10.0;
|
||||
params.driving_lane_penalty = self.panel.spinner("driving lane penalty") as f64 / 10.0;
|
||||
(TripMode::Bike, params)
|
||||
}
|
||||
|
||||
fn recalc_paths(&mut self, ctx: &mut EventCtx, app: &App) {
|
||||
let mode = match self.controls_to_profile() {
|
||||
RoutingProfile::Driving => TripMode::Drive,
|
||||
RoutingProfile::Walking => TripMode::Walk,
|
||||
RoutingProfile::Biking { .. } => TripMode::Bike,
|
||||
};
|
||||
let (mode, params) = self.controls_to_params();
|
||||
|
||||
if let Some((ref goal, _, ref mut preview)) = self.goal {
|
||||
*preview = Drawable::empty(ctx);
|
||||
if let Some(polygon) =
|
||||
TripEndpoint::path_req(self.start.clone(), goal.clone(), mode, &app.primary.map)
|
||||
.and_then(|req| app.primary.map.pathfind(req).ok())
|
||||
.and_then(|req| app.primary.map.pathfind_with_params(req, ¶ms).ok())
|
||||
.and_then(|path| path.trace(&app.primary.map))
|
||||
.map(|pl| pl.make_polygons(NORMAL_LANE_THICKNESS))
|
||||
{
|
||||
@ -78,18 +74,21 @@ impl State<App> for RouteExplorer {
|
||||
return Transition::Pop;
|
||||
}
|
||||
"bikes" => {
|
||||
let controls = profile_to_controls(ctx, &RoutingProfile::default_biking());
|
||||
self.panel.replace(ctx, "profile", controls);
|
||||
let controls =
|
||||
params_to_controls(ctx, TripMode::Bike, &RoutingParams::default());
|
||||
self.panel.replace(ctx, "params", controls);
|
||||
self.recalc_paths(ctx, app);
|
||||
}
|
||||
"cars" => {
|
||||
let controls = profile_to_controls(ctx, &RoutingProfile::Driving);
|
||||
self.panel.replace(ctx, "profile", controls);
|
||||
let controls =
|
||||
params_to_controls(ctx, TripMode::Drive, &RoutingParams::default());
|
||||
self.panel.replace(ctx, "params", controls);
|
||||
self.recalc_paths(ctx, app);
|
||||
}
|
||||
"pedestrians" => {
|
||||
let controls = profile_to_controls(ctx, &RoutingProfile::Walking);
|
||||
self.panel.replace(ctx, "profile", controls);
|
||||
let controls =
|
||||
params_to_controls(ctx, TripMode::Walk, &RoutingParams::default());
|
||||
self.panel.replace(ctx, "params", controls);
|
||||
self.recalc_paths(ctx, app);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
@ -178,65 +177,37 @@ impl State<App> for RouteExplorer {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Move to map_model
|
||||
// TODO Not sure an enum makes sense, based on how we're still going to be toggling based on
|
||||
// PathConstraints.
|
||||
enum RoutingProfile {
|
||||
Driving,
|
||||
Biking {
|
||||
bike_lane_penalty: f64,
|
||||
bus_lane_penalty: f64,
|
||||
driving_lane_penalty: f64,
|
||||
},
|
||||
Walking,
|
||||
}
|
||||
|
||||
impl RoutingProfile {
|
||||
fn default_biking() -> RoutingProfile {
|
||||
RoutingProfile::Biking {
|
||||
bike_lane_penalty: 1.0,
|
||||
bus_lane_penalty: 1.1,
|
||||
driving_lane_penalty: 1.5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn profile_to_controls(ctx: &mut EventCtx, profile: &RoutingProfile) -> Widget {
|
||||
fn params_to_controls(ctx: &mut EventCtx, mode: TripMode, params: &RoutingParams) -> Widget {
|
||||
let mut rows = vec![Widget::custom_row(vec![
|
||||
ctx.style()
|
||||
.btn_plain_light_icon("system/assets/meters/bike.svg")
|
||||
.disabled(matches!(profile, RoutingProfile::Biking { .. }))
|
||||
.disabled(mode == TripMode::Bike)
|
||||
.build_widget(ctx, "bikes"),
|
||||
ctx.style()
|
||||
.btn_plain_light_icon("system/assets/meters/car.svg")
|
||||
.disabled(matches!(profile, RoutingProfile::Driving))
|
||||
.disabled(mode == TripMode::Drive)
|
||||
.build_widget(ctx, "cars"),
|
||||
ctx.style()
|
||||
.btn_plain_light_icon("system/assets/meters/pedestrian.svg")
|
||||
.disabled(matches!(profile, RoutingProfile::Walking))
|
||||
.disabled(mode == TripMode::Walk)
|
||||
.build_widget(ctx, "pedestrians"),
|
||||
])
|
||||
.evenly_spaced()];
|
||||
if let RoutingProfile::Biking {
|
||||
bike_lane_penalty,
|
||||
bus_lane_penalty,
|
||||
driving_lane_penalty,
|
||||
} = profile
|
||||
{
|
||||
if mode == TripMode::Bike {
|
||||
// TODO Spinners that natively understand a floating point range with a given precision
|
||||
rows.push(Widget::row(vec![
|
||||
"Bike lane penalty:".draw_text(ctx).margin_right(20),
|
||||
Spinner::new(ctx, (0, 20), (*bike_lane_penalty * 10.0) as isize)
|
||||
Spinner::new(ctx, (0, 20), (params.bike_lane_penalty * 10.0) as isize)
|
||||
.named("bike lane penalty"),
|
||||
]));
|
||||
rows.push(Widget::row(vec![
|
||||
"Bus lane penalty:".draw_text(ctx).margin_right(20),
|
||||
Spinner::new(ctx, (0, 20), (*bus_lane_penalty * 10.0) as isize)
|
||||
Spinner::new(ctx, (0, 20), (params.bus_lane_penalty * 10.0) as isize)
|
||||
.named("bus lane penalty"),
|
||||
]));
|
||||
rows.push(Widget::row(vec![
|
||||
"Driving lane penalty:".draw_text(ctx).margin_right(20),
|
||||
Spinner::new(ctx, (0, 20), (*driving_lane_penalty * 10.0) as isize)
|
||||
Spinner::new(ctx, (0, 20), (params.driving_lane_penalty * 10.0) as isize)
|
||||
.named("driving lane penalty"),
|
||||
]));
|
||||
}
|
||||
|
@ -174,6 +174,7 @@ impl UberTurnViewer {
|
||||
map.get_l(t.src),
|
||||
map.get_t(*t),
|
||||
PathConstraints::Car,
|
||||
map.routing_params(),
|
||||
map,
|
||||
);
|
||||
}
|
||||
|
@ -83,7 +83,13 @@ pub fn all_vehicle_costs_from(
|
||||
if let Some(start_lane) = bldg_to_lane.get(&start) {
|
||||
let graph = build_graph_for_vehicles(map, constraints);
|
||||
let cost_per_lane = petgraph::algo::dijkstra(&graph, *start_lane, None, |(_, _, turn)| {
|
||||
driving_cost(map.get_l(turn.src), map.get_t(*turn), constraints, map)
|
||||
driving_cost(
|
||||
map.get_l(turn.src),
|
||||
map.get_t(*turn),
|
||||
constraints,
|
||||
map.routing_params(),
|
||||
map,
|
||||
)
|
||||
});
|
||||
for (b, lane) in bldg_to_lane {
|
||||
if let Some(meters) = cost_per_lane.get(&lane) {
|
||||
|
@ -56,7 +56,7 @@ pub use crate::objects::turn::{
|
||||
pub use crate::objects::zone::{AccessRestrictions, Zone};
|
||||
pub use crate::pathfind::uber_turns::{IntersectionCluster, UberTurn, UberTurnGroup};
|
||||
use crate::pathfind::Pathfinder;
|
||||
pub use crate::pathfind::{Path, PathConstraints, PathRequest, PathStep};
|
||||
pub use crate::pathfind::{Path, PathConstraints, PathRequest, PathStep, RoutingParams};
|
||||
pub use crate::traversable::{Position, Traversable};
|
||||
|
||||
mod city;
|
||||
|
@ -15,7 +15,8 @@ use crate::{
|
||||
osm, Area, AreaID, AreaType, Building, BuildingID, BuildingType, BusRoute, BusRouteID, BusStop,
|
||||
BusStopID, ControlStopSign, ControlTrafficSignal, Intersection, IntersectionID, Lane, LaneID,
|
||||
LaneType, Map, MapEdits, MovementID, OffstreetParking, ParkingLot, ParkingLotID, Path,
|
||||
PathConstraints, PathRequest, Pathfinder, Position, Road, RoadID, Turn, TurnID, TurnType, Zone,
|
||||
PathConstraints, PathRequest, Pathfinder, Position, Road, RoadID, RoutingParams, Turn, TurnID,
|
||||
TurnType, Zone,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
@ -562,6 +563,12 @@ impl Map {
|
||||
assert!(!self.pathfinder_dirty);
|
||||
self.pathfinder.pathfind_avoiding_lanes(req, avoid, self)
|
||||
}
|
||||
pub fn pathfind_with_params(&self, req: PathRequest, params: &RoutingParams) -> Result<Path> {
|
||||
assert!(!self.pathfinder_dirty);
|
||||
self.pathfinder
|
||||
.pathfind_with_params(req.clone(), params, self)
|
||||
.ok_or_else(|| anyhow!("can't fulfill {}", req))
|
||||
}
|
||||
|
||||
pub fn should_use_transit(
|
||||
&self,
|
||||
@ -703,4 +710,13 @@ impl Map {
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the routing params baked into the map. Currently just hardcoded defaults.
|
||||
// Depending how this works out, we might require everybody to explicitly plumb routing params,
|
||||
// in which case it should be easy to look for all places calling this.
|
||||
pub fn routing_params(&self) -> &'static RoutingParams {
|
||||
&ROUTING_PARAMS
|
||||
}
|
||||
}
|
||||
|
||||
static ROUTING_PARAMS: RoutingParams = RoutingParams::default();
|
||||
|
@ -86,7 +86,13 @@ impl Zone {
|
||||
req.start.lane(),
|
||||
|l| l == req.end.lane(),
|
||||
|(_, _, turn)| {
|
||||
driving_cost(map.get_l(turn.src), map.get_t(*turn), req.constraints, map)
|
||||
driving_cost(
|
||||
map.get_l(turn.src),
|
||||
map.get_t(*turn),
|
||||
req.constraints,
|
||||
map.routing_params(),
|
||||
map,
|
||||
)
|
||||
},
|
||||
|_| 0.0,
|
||||
)?;
|
||||
|
@ -6,13 +6,13 @@ use petgraph::graphmap::DiGraphMap;
|
||||
|
||||
use crate::pathfind::driving::driving_cost;
|
||||
use crate::pathfind::walking::{walking_cost, WalkingNode};
|
||||
use crate::{LaneID, Map, Path, PathConstraints, PathRequest, PathStep, TurnID};
|
||||
use crate::{LaneID, Map, Path, PathConstraints, PathRequest, PathStep, RoutingParams, TurnID};
|
||||
|
||||
// TODO These should maybe keep the DiGraphMaps as state. It's cheap to recalculate it for edits.
|
||||
|
||||
pub fn simple_pathfind(req: &PathRequest, map: &Map) -> Option<Path> {
|
||||
pub fn simple_pathfind(req: &PathRequest, params: &RoutingParams, map: &Map) -> Option<Path> {
|
||||
let graph = build_graph_for_vehicles(map, req.constraints);
|
||||
calc_path(graph, req, map)
|
||||
calc_path(graph, req, params, map)
|
||||
}
|
||||
|
||||
pub fn build_graph_for_vehicles(
|
||||
@ -45,15 +45,28 @@ pub fn pathfind_avoiding_lanes(
|
||||
}
|
||||
}
|
||||
|
||||
calc_path(graph, &req, map)
|
||||
calc_path(graph, &req, map.routing_params(), map)
|
||||
}
|
||||
|
||||
fn calc_path(graph: DiGraphMap<LaneID, TurnID>, req: &PathRequest, map: &Map) -> Option<Path> {
|
||||
fn calc_path(
|
||||
graph: DiGraphMap<LaneID, TurnID>,
|
||||
req: &PathRequest,
|
||||
params: &RoutingParams,
|
||||
map: &Map,
|
||||
) -> Option<Path> {
|
||||
let (_, path) = petgraph::algo::astar(
|
||||
&graph,
|
||||
req.start.lane(),
|
||||
|l| l == req.end.lane(),
|
||||
|(_, _, turn)| driving_cost(map.get_l(turn.src), map.get_t(*turn), req.constraints, map),
|
||||
|(_, _, turn)| {
|
||||
driving_cost(
|
||||
map.get_l(turn.src),
|
||||
map.get_t(*turn),
|
||||
req.constraints,
|
||||
params,
|
||||
map,
|
||||
)
|
||||
},
|
||||
|_| 0.0,
|
||||
)?;
|
||||
let mut steps = Vec::new();
|
||||
|
@ -10,7 +10,9 @@ use abstutil::MultiMap;
|
||||
|
||||
use crate::pathfind::node_map::{deserialize_nodemap, NodeMap};
|
||||
use crate::pathfind::uber_turns::{IntersectionCluster, UberTurn};
|
||||
use crate::{Lane, LaneID, Map, Path, PathConstraints, PathRequest, PathStep, Turn, TurnID};
|
||||
use crate::{
|
||||
Lane, LaneID, Map, Path, PathConstraints, PathRequest, PathStep, RoutingParams, Turn, TurnID,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct VehiclePathfinder {
|
||||
@ -183,7 +185,13 @@ fn make_input_graph(
|
||||
input_graph.add_edge(
|
||||
from,
|
||||
nodes.get(Node::Lane(turn.id.dst)),
|
||||
round(driving_cost(l, turn, constraints, map)),
|
||||
round(driving_cost(
|
||||
l,
|
||||
turn,
|
||||
constraints,
|
||||
map.routing_params(),
|
||||
map,
|
||||
)),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@ -193,7 +201,13 @@ fn make_input_graph(
|
||||
|
||||
let mut sum_cost = 0.0;
|
||||
for t in &ut.path {
|
||||
sum_cost += driving_cost(map.get_l(t.src), map.get_t(*t), constraints, map);
|
||||
sum_cost += driving_cost(
|
||||
map.get_l(t.src),
|
||||
map.get_t(*t),
|
||||
constraints,
|
||||
map.routing_params(),
|
||||
map,
|
||||
);
|
||||
}
|
||||
input_graph.add_edge(from, nodes.get(Node::UberTurn(*idx)), round(sum_cost));
|
||||
input_graph.add_edge(
|
||||
@ -219,7 +233,13 @@ fn make_input_graph(
|
||||
}
|
||||
|
||||
/// Different unit based on constraints.
|
||||
pub fn driving_cost(lane: &Lane, turn: &Turn, constraints: PathConstraints, map: &Map) -> f64 {
|
||||
pub fn driving_cost(
|
||||
lane: &Lane,
|
||||
turn: &Turn,
|
||||
constraints: PathConstraints,
|
||||
params: &RoutingParams,
|
||||
map: &Map,
|
||||
) -> f64 {
|
||||
// TODO Could cost turns differently.
|
||||
|
||||
let base = match constraints {
|
||||
@ -239,12 +259,12 @@ pub fn driving_cost(lane: &Lane, turn: &Turn, constraints: PathConstraints, map:
|
||||
// TODO Prefer bike lanes, then bus lanes, then driving lanes. For now, express that as
|
||||
// an extra cost.
|
||||
let lt_penalty = if lane.is_biking() {
|
||||
1.0
|
||||
params.bike_lane_penalty
|
||||
} else if lane.is_bus() {
|
||||
1.1
|
||||
params.bus_lane_penalty
|
||||
} else {
|
||||
assert!(lane.is_driving());
|
||||
1.5
|
||||
params.driving_lane_penalty
|
||||
};
|
||||
|
||||
// 1m resolution is fine
|
||||
|
@ -639,3 +639,24 @@ fn validate_restrictions(map: &Map, steps: &Vec<PathStep>) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tuneable parameters for all types of routing.
|
||||
// These will maybe become part of the PathRequest later, but that's an extremely invasive and
|
||||
// space-expensive change right now.
|
||||
#[derive(PartialEq)]
|
||||
pub struct RoutingParams {
|
||||
// For bike routing
|
||||
pub bike_lane_penalty: f64,
|
||||
pub bus_lane_penalty: f64,
|
||||
pub driving_lane_penalty: f64,
|
||||
}
|
||||
|
||||
impl RoutingParams {
|
||||
pub const fn default() -> RoutingParams {
|
||||
RoutingParams {
|
||||
bike_lane_penalty: 1.0,
|
||||
bus_lane_penalty: 1.1,
|
||||
driving_lane_penalty: 1.5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ use crate::pathfind::walking::{one_step_walking_path, walking_path_to_steps};
|
||||
use crate::pathfind::{dijkstra, WalkingNode};
|
||||
use crate::{
|
||||
BusRouteID, BusStopID, Intersection, LaneID, Map, Path, PathConstraints, PathRequest, Position,
|
||||
TurnID, Zone,
|
||||
RoutingParams, TurnID, Zone,
|
||||
};
|
||||
|
||||
/// Most of the time, prefer using the faster contraction hierarchies. But sometimes, callers can
|
||||
@ -25,6 +25,17 @@ impl Pathfinder {
|
||||
/// Finds a path from a start to an end for a certain type of agent. Handles requests that
|
||||
/// start or end inside access-restricted zones.
|
||||
pub fn pathfind(&self, req: PathRequest, map: &Map) -> Option<Path> {
|
||||
self.pathfind_with_params(req, map.routing_params(), map)
|
||||
}
|
||||
|
||||
/// Finds a path from a start to an end for a certain type of agent. Handles requests that
|
||||
/// start or end inside access-restricted zones. May use custom routing parameters.
|
||||
pub fn pathfind_with_params(
|
||||
&self,
|
||||
req: PathRequest,
|
||||
params: &RoutingParams,
|
||||
map: &Map,
|
||||
) -> Option<Path> {
|
||||
if req.start.lane() == req.end.lane() && req.constraints == PathConstraints::Pedestrian {
|
||||
return Some(one_step_walking_path(&req, map));
|
||||
}
|
||||
@ -104,7 +115,7 @@ impl Pathfinder {
|
||||
let steps = walking_path_to_steps(self.simple_walking_path(&req, map)?, map);
|
||||
return Some(Path::new(map, steps, req, Vec::new()));
|
||||
}
|
||||
self.simple_pathfind(&req, map)
|
||||
self.simple_pathfind(&req, params, map)
|
||||
}
|
||||
|
||||
pub fn pathfind_avoiding_lanes(
|
||||
@ -138,9 +149,22 @@ impl Pathfinder {
|
||||
}
|
||||
|
||||
// Doesn't handle zones or pedestrians
|
||||
fn simple_pathfind(&self, req: &PathRequest, map: &Map) -> Option<Path> {
|
||||
fn simple_pathfind(
|
||||
&self,
|
||||
req: &PathRequest,
|
||||
params: &RoutingParams,
|
||||
map: &Map,
|
||||
) -> Option<Path> {
|
||||
if params != &RoutingParams::default() {
|
||||
// If the params differ from the defaults, the CHs won't match. This should only be
|
||||
// happening from the debug UI; be very obnoxious if we start calling it from the
|
||||
// simulation or something else.
|
||||
warn!("Pathfinding for {} with custom params", req);
|
||||
return dijkstra::simple_pathfind(req, params, map);
|
||||
}
|
||||
|
||||
match self {
|
||||
Pathfinder::Dijkstra => dijkstra::simple_pathfind(req, map),
|
||||
Pathfinder::Dijkstra => dijkstra::simple_pathfind(req, params, map),
|
||||
Pathfinder::CH(ref p) => p.simple_pathfind(req, map),
|
||||
}
|
||||
}
|
||||
@ -225,7 +249,7 @@ impl Pathfinder {
|
||||
}
|
||||
|
||||
let mut interior_path = zone.pathfind(interior_req, map)?;
|
||||
let main_path = self.simple_pathfind(&req, map)?;
|
||||
let main_path = self.simple_pathfind(&req, map.routing_params(), map)?;
|
||||
interior_path.append(main_path, map);
|
||||
Some(interior_path)
|
||||
}
|
||||
@ -305,7 +329,7 @@ impl Pathfinder {
|
||||
}
|
||||
|
||||
let interior_path = zone.pathfind(interior_req, map)?;
|
||||
let mut main_path = self.simple_pathfind(&req, map)?;
|
||||
let mut main_path = self.simple_pathfind(&req, map.routing_params(), map)?;
|
||||
main_path.append(interior_path, map);
|
||||
main_path.orig_req = orig_req;
|
||||
Some(main_path)
|
||||
|
@ -385,6 +385,7 @@ fn transit_input_graph(
|
||||
}
|
||||
|
||||
/// The cost is time in seconds, rounded to a usize
|
||||
// TODO Plumb RoutingParams here, but first, need to also plumb in the turn or lane.
|
||||
pub fn walking_cost(dist: Distance) -> usize {
|
||||
let walking_speed = Speed::meters_per_second(1.34);
|
||||
let time = dist / walking_speed;
|
||||
|
Loading…
Reference in New Issue
Block a user