Skip to content

Commit

Permalink
Start a coverage mode to show gaps around some amenity. #5
Browse files Browse the repository at this point in the history
  • Loading branch information
dabreegster committed Dec 27, 2024
1 parent d64bb5e commit 5ee3903
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 6 deletions.
34 changes: 32 additions & 2 deletions backend/src/isochrone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,50 @@ pub enum Style {
Contours,
}

pub enum Source {
Single(Coord),
FromAmenity(String),
}

pub fn calculate(
graph: &Graph,
amenities: &Amenities,
req: Coord,
source: Source,
profile: ProfileID,
style: Style,
public_transit: bool,
start_time: NaiveTime,
limit: Duration,
mut timer: Timer,
) -> Result<String> {
let mut starts = Vec::new();
match source {
Source::Single(pt) => {
starts.push(graph.snap_to_road(pt, profile).intersection);
}
Source::FromAmenity(kind) => {
for (r, lists) in amenities.per_road.iter().enumerate() {
for a in &lists[profile.0] {
let amenity = &amenities.amenities[a.0];
if amenity.kind == kind {
let road = &graph.roads[r];
// TODO Which intersection is closer? Just start from either
starts.push(road.src_i);
starts.push(road.dst_i);
}
}
}
starts.sort();
starts.dedup();
if starts.is_empty() {
bail!("No amenities of kind {kind}");
}
}
}

timer.step("get_costs");
let cost_per_road = graph.get_costs(
vec![graph.snap_to_road(req, profile).intersection],
starts,
profile,
public_transit,
start_time,
Expand Down
17 changes: 13 additions & 4 deletions backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,16 @@ impl MapModel {
#[wasm_bindgen(js_name = isochrone)]
pub fn isochrone(&self, input: JsValue) -> Result<String, JsValue> {
let req: IsochroneRequest = serde_wasm_bindgen::from_value(input)?;
let start = self
.graph
.mercator
.pt_to_mercator(Coord { x: req.x, y: req.y });
let start = if req.from_amenity.is_empty() {
isochrone::Source::Single(
self.graph
.mercator
.pt_to_mercator(Coord { x: req.x, y: req.y }),
)
} else {
isochrone::Source::FromAmenity(req.from_amenity)
};

let profile = self.parse_profile(&req.profile)?;
isochrone::calculate(
&self.graph,
Expand Down Expand Up @@ -331,6 +337,9 @@ pub struct IsochroneRequest {
// TODO Rename lon, lat to be clear?
x: f64,
y: f64,
// TODO Improve this API -- it's an enum, either a single start, or from every amenity
from_amenity: String,

profile: String,
transit: bool,
style: String,
Expand Down
3 changes: 3 additions & 0 deletions web/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import RouteMode from "./RouteMode.svelte";
import DebugRouteMode from "./DebugRouteMode.svelte";
import ScoreMode from "./ScoreMode.svelte";
import CoverageMode from "./CoverageMode.svelte";
import {
map as mapStore,
mode,
Expand Down Expand Up @@ -171,6 +172,8 @@
<RouteMode />
{:else if $mode.kind == "score"}
<ScoreMode />
{:else if $mode.kind == "coverage"}
<CoverageMode />
{:else if $mode.kind == "debug-route"}
<DebugRouteMode
debugGj={$mode.debugGj}
Expand Down
148 changes: 148 additions & 0 deletions web/src/CoverageMode.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<script lang="ts">
import { PickProfile, NavBar } from "./common";
import { colorScale } from "./colors";
import type { FeatureCollection } from "geojson";
import { SymbolLayer, GeoJSON, FillLayer, LineLayer } from "svelte-maplibre";
import { SplitComponent } from "svelte-utils/top_bar_layout";
import {
backend,
profile,
type Profile,
startTime,
coverageMins,
} from "./stores";
import { SequentialLegend, notNull } from "svelte-utils";
import { Popup, makeColorRamp, isLine, isPolygon } from "svelte-utils/map";
let fromAmenity = "bicycle_parking";
// TODO Generalize; show source amenity
let showParking = true;
let style = "Roads";
let isochroneGj: FeatureCollection | null = null;
let err = "";
async function updateIsochrone(
_x: string,
_y: Profile,
_z: string,
_t: string,
_im: number,
) {
try {
isochroneGj = await $backend!.isochroneFromAmenity({
fromAmenity,
profile: $profile,
style,
startTime: $startTime,
maxSeconds: 60 * $coverageMins,
});
err = "";
} catch (err: any) {
isochroneGj = null;
err = err.toString();
}
}
$: updateIsochrone(fromAmenity, $profile, style, $startTime, $coverageMins);
$: limits = Array.from(Array(6).keys()).map(
(i) => (($coverageMins * 60) / (6 - 1)) * i,
);
</script>

<SplitComponent>
<div slot="top">
<NavBar />
</div>

<div slot="sidebar">
<h2>Coverage mode</h2>

<label>
<input type="checkbox" bind:checked={showParking} />
Show parking
</label>

<PickProfile bind:profile={$profile} />

<label>
Start time (PT only)
<input
type="time"
bind:value={$startTime}
disabled={$profile != "transit"}
/>
</label>

<label
>Draw:
<select bind:value={style}>
<option value="Roads">Roads</option>
<option value="Grid">Grid</option>
<option value="Contours">Contours</option>
</select>
</label>

<label
>Minutes away
<input type="number" bind:value={$coverageMins} min="1" max="30" />
</label>
<SequentialLegend {colorScale} limits={limits.map((l) => l / 60)} />
{#if err}
<p>{err}</p>
{/if}
</div>

<div slot="map">
{#if isochroneGj}
<GeoJSON data={isochroneGj} generateId>
<LineLayer
id="isochrone"
filter={isLine}
paint={{
"line-width": 2,
"line-color": makeColorRamp(
["get", "cost_seconds"],
limits,
colorScale,
),
"line-opacity": 0.5,
}}
eventsIfTopMost
>
<Popup openOn="hover" let:props>
{(props.cost_seconds / 60).toFixed(1)} minutes away
</Popup>
</LineLayer>

<FillLayer
id="isochrone-contours"
filter={isPolygon}
paint={{
"fill-color": makeColorRamp(
["get", "min_seconds"],
limits,
colorScale,
),
"fill-opacity": 0.5,
}}
/>
</GeoJSON>

{#await notNull($backend).renderAmenities() then data}
<GeoJSON {data}>
<SymbolLayer
filter={["==", ["get", "amenity_kind"], "bicycle_parking"]}
layout={{
"icon-image": "cycle_parking",
"icon-size": 1.0,
"icon-allow-overlap": true,
visibility: showParking ? "visible" : "none",
}}
/>
</GeoJSON>
{/await}
{/if}
</div>
</SplitComponent>
7 changes: 7 additions & 0 deletions web/src/common/NavBar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@
>
</li>

<li>
<button
on:click={() => ($mode = { kind: "coverage" })}
disabled={$mode.kind == "coverage"}>Coverage</button
>
</li>

<li>
<button
on:click={() => ($mode = { kind: "upload-route" })}
Expand Down
2 changes: 2 additions & 0 deletions web/src/stores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type Mode =
| { kind: "isochrone" }
| { kind: "route" }
| { kind: "score" }
| { kind: "coverage" }
| {
kind: "debug-route";
debugGj: FeatureCollection;
Expand Down Expand Up @@ -45,6 +46,7 @@ export let useHeuristic = writable(true);
export let showRouteBuffer = writable(false);
export let showRouteBufferPopulation = writable(false);
export let isochroneMins = writable(15);
export let coverageMins = writable(1);
export let bufferMins = writable(5);

// TODO Does this need to be a store?
Expand Down
26 changes: 26 additions & 0 deletions web/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,32 @@ export class Backend {
this.inner.isochrone({
x: req.start.lng,
y: req.start.lat,
from_amenity: "",
profile: req.profile == "transit" ? "foot" : req.profile,
transit: req.profile == "transit",
style: req.style,
start_time: req.startTime,
max_seconds: req.maxSeconds,
}),
);
}

isochroneFromAmenity(req: {
fromAmenity: string;
profile: Profile;
style: string;
startTime: string;
maxSeconds: number;
}): FeatureCollection {
if (!this.inner) {
throw new Error("Backend used without a file loaded");
}

return JSON.parse(
this.inner.isochrone({
x: 0,
y: 0,
from_amenity: req.fromAmenity,
profile: req.profile == "transit" ? "foot" : req.profile,
transit: req.profile == "transit",
style: req.style,
Expand Down

0 comments on commit 5ee3903

Please sign in to comment.