Skip to content

Commit

Permalink
View shortcuts per road... a start. The backend part is trivial to
Browse files Browse the repository at this point in the history
write, the frontend revisits a bunch of problems. Start with something
kind of working but very verbose.
  • Loading branch information
dabreegster committed Dec 22, 2023
1 parent ceb02e4 commit 08c662a
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 4 deletions.
18 changes: 17 additions & 1 deletion backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ extern crate log;
use std::sync::Once;

use geo::{Coord, Polygon};
use geojson::{Feature, GeoJson};
use geojson::{Feature, GeoJson, Geometry};
use serde::Deserialize;
use wasm_bindgen::prelude::*;

Expand Down Expand Up @@ -151,6 +151,22 @@ impl LTN {
.map_err(err_to_js)?,
)
}

#[wasm_bindgen(js_name = getShortcutsCrossingRoad)]
pub fn get_shortcuts_crossing_road(&self, road: usize) -> Result<String, JsValue> {
Ok(serde_json::to_string(&GeoJson::from(
Shortcuts::new(&self.map, self.neighbourhood.as_ref().unwrap())
.subset(RoadID(road))
.into_iter()
.map(|path| {
Feature::from(Geometry::from(
&self.map.mercator.to_wgs84(&path.geometry(&self.map)),
))
})
.collect::<Vec<_>>(),
))
.map_err(err_to_js)?)
}
}

#[derive(Deserialize)]
Expand Down
41 changes: 39 additions & 2 deletions backend/src/shortcuts.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
use std::collections::HashMap;

use fast_paths::InputGraph;
use geo::LineString;

use crate::{MapModel, Neighbourhood, NodeMap, RoadID};
use crate::{IntersectionID, MapModel, Neighbourhood, NodeMap, RoadID};

pub struct Shortcuts {
pub paths: Vec<Path>,
pub count_per_road: HashMap<RoadID, usize>,
}

pub struct Path {
steps: Vec<(RoadID, IntersectionID, IntersectionID)>,
}

impl Shortcuts {
pub fn new(map: &MapModel, neighbourhood: &Neighbourhood) -> Self {
let mut input_graph = InputGraph::new();
Expand All @@ -30,22 +36,53 @@ impl Shortcuts {
let ch = fast_paths::prepare(&input_graph);
let mut path_calc = fast_paths::create_calculator(&ch);

let mut paths = Vec::new();
let mut count_per_road = HashMap::new();
for start in &neighbourhood.border_intersections {
for end in &neighbourhood.border_intersections {
if let (Some(i1), Some(i2)) = (node_map.get(*start), node_map.get(*end)) {
if let Some(path) = path_calc.calc_path(&ch, i1, i2) {
let mut steps = Vec::new();
for pair in path.get_nodes().windows(2) {
let i1 = node_map.translate_id(pair[0]);
let i2 = node_map.translate_id(pair[1]);
let road = map.find_edge(i1, i2);
steps.push((road.id, i1, i2));
*count_per_road.entry(road.id).or_insert(0) += 1;
}
paths.push(Path { steps });
}
}
}
}

Self { count_per_road }
Self {
paths,
count_per_road,
}
}

pub fn subset(&self, crosses: RoadID) -> Vec<&Path> {
self.paths
.iter()
.filter(|path| path.steps.iter().any(|(r, _, _)| *r == crosses))
.collect()
}
}

impl Path {
pub fn geometry(&self, map: &MapModel) -> LineString {
let mut pts = Vec::new();
for (r, i1, i2) in &self.steps {
let road = map.get_r(*r);
if *i1 == road.src_i {
pts.extend(road.linestring.0.clone());
} else {
let mut rev = road.linestring.0.clone();
rev.reverse();
pts.extend(rev);
}
}
LineString::new(pts)
}
}
10 changes: 10 additions & 0 deletions web/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import NeighbourhoodLayer from "./NeighbourhoodLayer.svelte";
import NeighbourhoodMode from "./NeighbourhoodMode.svelte";
import NetworkLayer from "./NetworkLayer.svelte";
import ViewShortcutsLayer from "./ViewShortcutsLayer.svelte";
import ViewShortcutsMode from "./ViewShortcutsMode.svelte";
let offlineMode = true;
let mapStyle = offlineMode
Expand All @@ -35,6 +37,10 @@
undoLength: number;
redoLength: number;
rerender: number;
}
| {
mode: "view-shortcuts";
prevMode: Mode;
};
let mode = {
Expand Down Expand Up @@ -319,6 +325,8 @@
<p>Draw the boundary...</p>
{:else if mode.mode == "neighbourhood"}
<NeighbourhoodMode bind:mode {app} {setBoundaryMode} />
{:else if mode.mode == "view-shortcuts"}
<ViewShortcutsMode bind:mode {app} prevMode={mode.prevMode} {map} />
{/if}
</div>
<div slot="main" style="position:relative; width: 100%; height: 100vh;">
Expand All @@ -339,6 +347,8 @@
rerender={mode.rerender}
{offlineMode}
/>
{:else if mode.mode == "view-shortcuts"}
<ViewShortcutsLayer {app} />
{/if}
{/if}
</MapLibre>
Expand Down
3 changes: 2 additions & 1 deletion web/src/NeighbourhoodLayer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@
}
onDestroy(() => {
stopAddingFilter();
app.unsetNeighbourhood();
// TODO Then we can't "nest" ViewShortcuts beneath this
//app.unsetNeighbourhood();
});
function onClick(e: MapMouseEvent) {
render(JSON.parse(app.addModalFilter(e.lngLat)));
Expand Down
5 changes: 5 additions & 0 deletions web/src/NeighbourhoodMode.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@
disabled={mode.addingFilter}>Add a modal filter</button
>
</div>
<div>
<button on:click={() => (mode = { mode: "view-shortcuts", prevMode: mode })}
>View shortcuts</button
>
</div>

<div>
<button disabled={mode.undoLength == 0} on:click={undo}>
Expand Down
40 changes: 40 additions & 0 deletions web/src/ViewShortcutsLayer.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<script lang="ts">
import { LTN } from "backend";
import { GeoJSON, LineLayer } from "svelte-maplibre";
import { choseRoad, state } from "./stores";
export let app: LTN;
</script>

{#if $state.state == "neutral"}
<GeoJSON data={JSON.parse(app.render())}>
<LineLayer
paint={{
"line-width": 5,
"line-color": "black",
}}
on:click={(e) => choseRoad(app, e.detail.features[0].properties.id)}
hoverCursor="pointer"
/>
</GeoJSON>
{:else if $state.state == "chose-road"}
{#if $state.shortcutIndex == null}
<GeoJSON data={$state.gj}>
<LineLayer
paint={{
"line-width": 5,
"line-color": "red",
}}
/>
</GeoJSON>
{:else}
<GeoJSON data={$state.gj.features[$state.shortcutIndex]}>
<LineLayer
paint={{
"line-width": 5,
"line-color": "red",
}}
/>
</GeoJSON>
{/if}
{/if}
65 changes: 65 additions & 0 deletions web/src/ViewShortcutsMode.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<script lang="ts">
import type { Map } from "maplibre-gl";
import { onDestroy, onMount } from "svelte";
import { state } from "./stores";
export let mode: Mode;
export let app: LTN;
export let prevMode: Mode;
export let map: Map;
// Initially set this
$state = { state: "neutral" };
onMount(() => {
map.keyboard.disable();
});
onDestroy(() => {
map.keyboard.enable();
});
function onKeyDown(e: KeyboardEvent) {
if ($state.state == "chose-road") {
if (e.key == "ArrowLeft" && $state.shortcutIndex != 0) {
e.stopPropagation();
$state.shortcutIndex--;
}
if (e.key == "ArrowRight") {
e.stopPropagation();
if ($state.shortcutIndex == null) {
$state.shortcutIndex = 0;
} else if ($state.shortcutIndex != $state.gj.features.length - 1) {
$state.shortcutIndex++;
}
}
}
}
function back() {
mode = prevMode;
}
</script>

<svelte:window on:keydown={onKeyDown} />

<div><button on:click={back}>Back to editing</button></div>

{#if $state.state == "neutral"}
<p>Click a road to see shortcuts</p>
{:else if $state.state == "chose-road"}
<div>
<button
disabled={$state.shortcutIndex == null || $state.shortcutIndex == 0}
on:click={() => $state.shortcutIndex--}
>
Prev
</button>
{$state.shortcutIndex} / {$state.gj.features.length}
<button
disabled={$state.shortcutIndex == $state.gj.features.length - 1}
on:click={() => $state.shortcutIndex++}
>
Next
</button>
</div>
{/if}
30 changes: 30 additions & 0 deletions web/src/stores.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { LTN } from "backend";
import { writable, type Writable } from "svelte/store";

export type State =
| {
state: "neutral";
}
| {
state: "chose-road";
road: number;
gj: FeatureCollection;
shortcutIndex: number | null;
};

export let state: Writable<State> = writable({ state: "neutral" });

export function choseRoad(app: LTN, road: number) {
let gj = JSON.parse(app.getShortcutsCrossingRoad(road));
if (gj.features.length == 0) {
window.alert("No shortcuts here");
return;
}

state.set({
state: "chose-road",
road,
gj,
shortcutIndex: null,
});
}

0 comments on commit 08c662a

Please sign in to comment.