Skip to content

Commit

Permalink
Start a very simple buffer-around-route feature
Browse files Browse the repository at this point in the history
  • Loading branch information
dabreegster committed Aug 15, 2024
1 parent fad6838 commit a8f075c
Show file tree
Hide file tree
Showing 10 changed files with 244 additions and 27 deletions.
2 changes: 1 addition & 1 deletion backend/src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ impl Road {
}

/// A position along a road, along with the closer intersection
#[derive(PartialEq)]
#[derive(Clone, Copy, PartialEq)]
pub struct Position {
pub road: RoadID,
pub fraction_along: f64,
Expand Down
10 changes: 5 additions & 5 deletions backend/src/isochrone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub fn calculate(
timer.step("get_costs");
let cost_per_road = get_costs(
graph,
req,
vec![graph.snap_to_road(req, mode).intersection],
mode,
public_transit,
start_time,
Expand Down Expand Up @@ -64,19 +64,19 @@ pub fn calculate(
// TODO Doesn't account for start/end distance along roads
pub fn get_costs(
graph: &Graph,
req: Coord,
starts: Vec<IntersectionID>,
mode: Mode,
public_transit: bool,
start_time: NaiveTime,
end_time: NaiveTime,
) -> HashMap<RoadID, Duration> {
let start = graph.snap_to_road(req, mode);

let mut visited: HashSet<IntersectionID> = HashSet::new();
let mut cost_per_road: HashMap<RoadID, Duration> = HashMap::new();
let mut queue: BinaryHeap<PriorityQueueItem<NaiveTime, IntersectionID>> = BinaryHeap::new();

queue.push(PriorityQueueItem::new(start_time, start.intersection));
for start in starts {
queue.push(PriorityQueueItem::new(start_time, start));
}

while let Some(current) = queue.pop() {
if visited.contains(&current.value) {
Expand Down
97 changes: 97 additions & 0 deletions backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,91 @@ impl MapModel {
self.route_from_req(&req)
}

#[wasm_bindgen(js_name = bufferRoute)]
pub fn buffer_route(&self, input: JsValue) -> Result<String, JsValue> {
let req: BufferRouteRequest = serde_wasm_bindgen::from_value(input)?;

// TODO Duplicating some route_from_req boilerplate
let mode = match req.mode.as_str() {
"car" => Mode::Car,
"bicycle" => Mode::Bicycle,
"foot" => Mode::Foot,
// For endpoint matching only
"transit" => Mode::Foot,
// TODO error plumbing
x => panic!("bad input {x}"),
};
let start = self.graph.snap_to_road(
self.graph.mercator.pt_to_mercator(Coord {
x: req.x1,
y: req.y1,
}),
mode,
);
let end = self.graph.snap_to_road(
self.graph.mercator.pt_to_mercator(Coord {
x: req.x2,
y: req.y2,
}),
mode,
);

let steps = if req.mode == "transit" {
todo!()
} else {
self.graph.router[mode]
.route_steps(&self.graph, start, end)
.map_err(err_to_js)?
};

let mut features = Vec::new();
let mut route_roads = HashSet::new();
let mut starts = HashSet::new();
for step in steps {
if let crate::route::PathStep::Road { road, .. } = step {
route_roads.insert(road);
let road = &self.graph.roads[road.0];
starts.insert(road.src_i);
starts.insert(road.dst_i);

// TODO Doesn't handle the exact start/end
let mut f = geojson::Feature::from(geojson::Geometry::from(
&self.graph.mercator.to_wgs84(&road.linestring),
));
f.set_property("kind", "route");
features.push(f);
}
}

let public_transit = false; // TODO
let start_time = NaiveTime::parse_from_str(&req.start_time, "%H:%M").map_err(err_to_js)?;
let limit = Duration::from_secs(req.max_seconds);
let cost_per_road = crate::isochrone::get_costs(
&self.graph,
starts.into_iter().collect(),
mode,
public_transit,
start_time,
start_time + limit,
);
for (r, cost) in cost_per_road {
if !route_roads.contains(&r) {
let mut f = geojson::Feature::from(geojson::Geometry::from(
&self
.graph
.mercator
.to_wgs84(&self.graph.roads[r.0].linestring),
));
f.set_property("kind", "buffer");
f.set_property("cost_seconds", cost.as_secs());
features.push(f);
}
}

let gj = geojson::GeoJson::from(features);
serde_json::to_string(&gj).map_err(err_to_js)
}

#[wasm_bindgen(js_name = score)]
pub fn score(
&self,
Expand Down Expand Up @@ -218,6 +303,18 @@ pub struct RouteRequest {
pub start_time: String,
}

#[derive(Deserialize)]
pub struct BufferRouteRequest {
pub x1: f64,
pub y1: f64,
pub x2: f64,
pub y2: f64,
pub mode: String,
pub use_heuristic: bool,
pub start_time: String,
pub max_seconds: u64,
}

#[derive(Deserialize)]
pub struct ScoreRequest {
poi_kinds: Vec<String>,
Expand Down
53 changes: 33 additions & 20 deletions backend/src/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,26 +51,13 @@ impl Router {
}
}

pub fn route(&self, graph: &Graph, start: Position, end: Position) -> Result<String> {
if start == end {
bail!("start = end");
}
if start.road == end.road {
// Just slice the one road
let mut slice = graph.roads[start.road.0]
.linestring
.line_split_twice(start.fraction_along, end.fraction_along)
.unwrap()
.into_second()
.unwrap();
if start.fraction_along > end.fraction_along {
slice.0.reverse();
}
let mut f = Feature::from(Geometry::from(&graph.mercator.to_wgs84(&slice)));
f.set_property("kind", "road");
return Ok(serde_json::to_string(&GeoJson::from(vec![f]))?);
}

// TODO This doesn't handle start=end cases
pub fn route_steps(
&self,
graph: &Graph,
start: Position,
end: Position,
) -> Result<Vec<PathStep>> {
let start_node = self.node_map.get(start.intersection).unwrap();
let end_node = self.node_map.get(end.intersection).unwrap();

Expand Down Expand Up @@ -110,6 +97,32 @@ impl Router {
}
}

Ok(steps)
}

// TODO Rename -- renders to GJ
pub fn route(&self, graph: &Graph, start: Position, end: Position) -> Result<String> {
if start == end {
bail!("start = end");
}
if start.road == end.road {
// Just slice the one road
let mut slice = graph.roads[start.road.0]
.linestring
.line_split_twice(start.fraction_along, end.fraction_along)
.unwrap()
.into_second()
.unwrap();
if start.fraction_along > end.fraction_along {
slice.0.reverse();
}
let mut f = Feature::from(Geometry::from(&graph.mercator.to_wgs84(&slice)));
f.set_property("kind", "road");
return Ok(serde_json::to_string(&GeoJson::from(vec![f]))?);
}

let steps = self.route_steps(graph, start, end)?;

// TODO Share code with PT?
let mut pts = Vec::new();
for (pos, step) in steps.into_iter().with_position() {
Expand Down
6 changes: 5 additions & 1 deletion backend/src/score.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ pub fn calculate(

let costs = crate::isochrone::get_costs(
graph,
amenity.point.into(),
vec![
graph
.snap_to_road(amenity.point.into(), Mode::Foot)
.intersection,
],
Mode::Foot,
false,
start_time,
Expand Down
3 changes: 3 additions & 0 deletions web/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import IsochroneMode from "./IsochroneMode.svelte";
import RouteMode from "./RouteMode.svelte";
import DebugRouteMode from "./DebugRouteMode.svelte";
import BufferRouteMode from "./BufferRouteMode.svelte";
import ScoreMode from "./ScoreMode.svelte";
import {
map as mapStore,
Expand Down Expand Up @@ -162,6 +163,8 @@
end={$mode.end}
routeGj={$mode.routeGj}
/>
{:else if $mode.kind == "buffer-route"}
<BufferRouteMode gj={$mode.gj} />
{/if}
{/if}
</MapLibre>
Expand Down
48 changes: 48 additions & 0 deletions web/src/BufferRouteMode.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<script lang="ts">
import { GeoJSON, LineLayer } from "svelte-maplibre";
import { SplitComponent } from "svelte-utils/top_bar_layout";
import { mode } from "./stores";
import type { FeatureCollection } from "geojson";
import { makeColorRamp, Popup } from "svelte-utils/map";
import { SequentialLegend } from "svelte-utils";
import { colorScale } from "./colors";
export let gj: FeatureCollection;
let limitsMinutes = [0, 1, 2, 3, 4, 5];
let limitsSeconds = limitsMinutes.map((x) => x * 60);
</script>

<SplitComponent>
<div slot="top">
<button on:click={() => ($mode = { kind: "route" })}>Back</button>
</div>
<div slot="sidebar">
<h2>Buffer around a route</h2>
<SequentialLegend {colorScale} limits={limitsMinutes} />
</div>
<div slot="map">
<GeoJSON data={gj}>
<LineLayer
paint={{
"line-width": 20,
"line-color": [
"case",
["==", ["get", "kind"], "route"],
"red",
makeColorRamp(["get", "cost_seconds"], limitsSeconds, colorScale),
],
"line-opacity": 0.5,
}}
>
<Popup openOn="hover" let:props>
{#if props.kind == "buffer"}
{(props.cost_seconds / 60).toFixed(1)} minutes away
{:else}
part of the route
{/if}
</Popup>
</LineLayer>
</GeoJSON>
</div>
</SplitComponent>
21 changes: 21 additions & 0 deletions web/src/RouteMode.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,25 @@
err = error.toString();
}
}
async function bufferRoute() {
try {
let bufferGj = await $backend!.bufferRoute({
start: $routeA!,
end: [$routeB!.lng, $routeB!.lat],
mode: $travelMode,
useHeuristic: $useHeuristic,
startTime: $startTime,
maxSeconds: 5 * 60,
});
$mode = {
kind: "buffer-route",
gj: bufferGj,
};
} catch (error: any) {
err = error.toString();
}
}
</script>

<SplitComponent>
Expand Down Expand Up @@ -115,6 +134,8 @@
>Watch how this route was found (PT only)</button
>

<button on:click={bufferRoute}>Buffer 5 mins around this route</button>

<ol>
{#each gj.features as f}
{@const props = notNull(f.properties)}
Expand Down
4 changes: 4 additions & 0 deletions web/src/stores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export type Mode =
start: { lng: number; lat: number };
end: { lng: number; lat: number };
routeGj: FeatureCollection;
}
| {
kind: "buffer-route";
gj: FeatureCollection;
};

export let mode: Writable<Mode> = writable({ kind: "title" });
Expand Down
27 changes: 27 additions & 0 deletions web/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,33 @@ export class Backend {
);
}

bufferRoute(req: {
// TODO LngLatLike doesn't work?
start: { lng: number; lat: number };
end: Position;
mode: TravelMode;
useHeuristic: boolean;
startTime: string;
maxSeconds: number;
}): FeatureCollection {
if (!this.inner) {
throw new Error("Backend used without a file loaded");
}

return JSON.parse(
this.inner.bufferRoute({
x1: req.start.lng,
y1: req.start.lat,
x2: req.end[0],
y2: req.end[1],
mode: req.mode,
use_heuristic: req.useHeuristic,
start_time: req.startTime,
max_seconds: req.maxSeconds,
}),
);
}

score(
req: {
poiKinds: string[];
Expand Down

0 comments on commit a8f075c

Please sign in to comment.