Skip to content

Commit

Permalink
Start a more generic isochrone implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
dabreegster committed Apr 28, 2024
1 parent d9d2b4b commit f3e9e75
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 23 deletions.
33 changes: 24 additions & 9 deletions backend/src/isochrone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ use crate::{MapModel, RoadID};
use anyhow::Result;
use geo::{Coord, EuclideanLength};

pub fn calculate(map: &MapModel, req: Coord) -> Result<String> {
use crate::Mode;

pub fn calculate(map: &MapModel, req: Coord, mode: Mode) -> Result<String> {
// 1km in cm
// TODO Use a real cost type
let limit = 1000 * 100;
let cost_per_road = get_costs(map, req, limit);
let cost_per_road = get_costs(map, req, mode, limit);

// Show cost per road
let mut features = Vec::new();
Expand All @@ -25,7 +27,8 @@ pub fn calculate(map: &MapModel, req: Coord) -> Result<String> {
Ok(x)
}

fn get_costs(map: &MapModel, req: Coord, limit: usize) -> HashMap<RoadID, usize> {
fn get_costs(map: &MapModel, req: Coord, mode: Mode, limit: usize) -> HashMap<RoadID, usize> {
// TODO This needs to be per mode
let start = map
.closest_intersection
.nearest_neighbor(&[req.x, req.y])
Expand All @@ -34,8 +37,9 @@ fn get_costs(map: &MapModel, req: Coord, limit: usize) -> HashMap<RoadID, usize>

let mut queue: BinaryHeap<PriorityQueueItem<usize, RoadID>> = BinaryHeap::new();
// TODO Match closest road. For now, start with all roads for the closest intersection
for r in &map.intersections[start.0].roads {
queue.push(PriorityQueueItem::new(0, *r));
// TODO Think through directions for this initial case. Going by road is strange.
for road in map.roads_per_intersection(start, mode) {
queue.push(PriorityQueueItem::new(0, road.id));
}

let mut cost_per_road: HashMap<RoadID, usize> = HashMap::new();
Expand All @@ -49,10 +53,21 @@ fn get_costs(map: &MapModel, req: Coord, limit: usize) -> HashMap<RoadID, usize>
cost_per_road.insert(current.value, current.cost);

let current_road = &map.roads[current.value.0];
for i in [current_road.src_i, current_road.dst_i] {
for r in &map.intersections[i.0].roads {
let cost = (100.0 * map.roads[r.0].linestring.euclidean_length()).round() as usize;
queue.push(PriorityQueueItem::new(current.cost + cost, *r));
// TODO Think through how this search should work with directions. This is assuming
// incorrectly we're starting from src_i.
let mut endpoints = Vec::new();
if current_road.allows_forwards(mode) {
endpoints.push(current_road.dst_i);
}
if current_road.allows_backwards(mode) {
endpoints.push(current_road.src_i);
}

for i in endpoints {
for road in map.roads_per_intersection(i, mode) {
// TODO Different cost per mode
let cost = (100.0 * road.linestring.euclidean_length()).round() as usize;
queue.push(PriorityQueueItem::new(current.cost + cost, road.id));
}
}
}
Expand Down
48 changes: 46 additions & 2 deletions backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,21 @@ impl fmt::Display for IntersectionID {
}
}

#[derive(Debug)]
#[derive(Clone, Copy, Debug)]
pub enum Direction {
Forwards,
Backwards,
Both,
None,
}

#[derive(Clone, Copy)]
pub enum Mode {
Car,
Bicycle,
Foot,
}

pub struct Road {
id: RoadID,
src_i: IntersectionID,
Expand All @@ -64,6 +71,7 @@ pub struct Road {
tags: Tags,

// A simplified view of who can access a road. All might be None (buses, trains ignored)
// TODO enum map?
access_car: Direction,
access_bicycle: Direction,
access_foot: Direction,
Expand Down Expand Up @@ -134,11 +142,46 @@ impl MapModel {
pub fn isochrone(&self, input: JsValue) -> Result<String, JsValue> {
let req: IsochroneRequest = serde_wasm_bindgen::from_value(input)?;
let start = self.mercator.pt_to_mercator(Coord { x: req.x, y: req.y });
isochrone::calculate(&self, start).map_err(err_to_js)
let mode = match req.mode.as_str() {
"car" => Mode::Car,
"bicycle" => Mode::Bicycle,
"foot" => Mode::Foot,
// TODO error plumbing
x => panic!("bad input {x}"),
};
isochrone::calculate(&self, start, mode).map_err(err_to_js)
}
}

impl MapModel {
fn roads_per_intersection(&self, i: IntersectionID, mode: Mode) -> impl Iterator<Item = &Road> {
self.intersections[i.0]
.roads
.iter()
.map(|r| &self.roads[r.0])
.filter(move |r| r.allows_forwards(mode) || r.allows_backwards(mode))
}
}

impl Road {
fn allows_forwards(&self, mode: Mode) -> bool {
let dir = match mode {
Mode::Car => self.access_car,
Mode::Bicycle => self.access_bicycle,
Mode::Foot => self.access_foot,
};
matches!(dir, Direction::Forwards | Direction::Both)
}

fn allows_backwards(&self, mode: Mode) -> bool {
let dir = match mode {
Mode::Car => self.access_car,
Mode::Bicycle => self.access_bicycle,
Mode::Foot => self.access_foot,
};
matches!(dir, Direction::Backwards | Direction::Both)
}

fn to_gj(&self, mercator: &Mercator) -> Feature {
let mut f = Feature::from(Geometry::from(&mercator.to_wgs84(&self.linestring)));
// TODO Rethink most of this -- it's debug info
Expand All @@ -160,6 +203,7 @@ impl Road {
pub struct IsochroneRequest {
x: f64,
y: f64,
mode: String,
}

fn err_to_js<E: std::fmt::Display>(err: E) -> JsValue {
Expand Down
28 changes: 18 additions & 10 deletions backend/src/scrape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ use anyhow::Result;
use rstar::RTree;
use utils::Tags;

use crate::{Direction, Intersection, IntersectionID, IntersectionLocation, MapModel, Road, RoadID};
use crate::{
Direction, Intersection, IntersectionID, IntersectionLocation, MapModel, Road, RoadID,
};

pub fn scrape_osm(input_bytes: &[u8]) -> Result<MapModel> {
let graph = utils::osm2graph::Graph::new(input_bytes, |tags| {
Expand Down Expand Up @@ -34,6 +36,7 @@ pub fn scrape_osm(input_bytes: &[u8]) -> Result<MapModel> {
node2: e.osm_node2,
linestring: e.linestring,

// TODO Should also look at any barriers
access_car: is_car_allowed(&e.osm_tags),
access_bicycle: is_bicycle_allowed(&e.osm_tags),
access_foot: is_foot_allowed(&e.osm_tags),
Expand All @@ -58,22 +61,27 @@ pub fn scrape_osm(input_bytes: &[u8]) -> Result<MapModel> {

// TODO Use Muv (rstar is pinning to an old smallvec). This is just placeholder.
fn is_car_allowed(tags: &Tags) -> Direction {
if tags.is_any("highway", vec![
"footway",
"steps",
"path",
"track",
"corridor",
"crossing",
"pedestrian",
]) {
if tags.is_any(
"highway",
vec![
"footway",
"steps",
"path",
"track",
"corridor",
"crossing",
"pedestrian",
],
) {
return Direction::None;
}
Direction::Both
}

fn is_bicycle_allowed(_tags: &Tags) -> Direction {
Direction::Both
}

fn is_foot_allowed(tags: &Tags) -> Direction {
if tags.is_any("highway", vec!["motorway", "motorway_link"]) {
return Direction::None;
Expand Down
11 changes: 9 additions & 2 deletions web/src/IsochroneMode.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
import { GeoJSON, LineLayer, Marker } from "svelte-maplibre";
import NetworkLayer from "./NetworkLayer.svelte";
import SplitComponent from "./SplitComponent.svelte";
import { mode, model } from "./stores";
import { makeColorRamp } from "./common";
import { mode, model, type TravelMode, filterForMode } from "./stores";
import { makeColorRamp, PickTravelMode } from "./common";
import { SequentialLegend, Popup } from "svelte-utils";
let travelMode: TravelMode = "foot";
// TODO Maybe need to do this when model changes
let bbox: number[] = Array.from($model!.getBounds());
let start = {
Expand All @@ -24,6 +26,7 @@
$model!.isochrone({
x: start.lng,
y: start.lat,
mode: travelMode,
}),
);
err = "";
Expand All @@ -47,10 +50,14 @@
<button on:click={() => ($mode = "title")}>Change study area</button>
<button on:click={() => ($mode = "debug")}>Debug OSM</button>
</div>

<p>
Move the pin to calculate an isochrone from that start. The cost is
distance in meters.
</p>

<PickTravelMode bind:travelMode />

<SequentialLegend {colorScale} {limits} />
{#if err}
<p>{err}</p>
Expand Down

0 comments on commit f3e9e75

Please sign in to comment.