Skip to content

Commit

Permalink
Map and manage arms per junction. #23
Browse files Browse the repository at this point in the history
Very clunky start, but minimally working.
  • Loading branch information
dabreegster committed May 8, 2024
1 parent c64dd32 commit 1d215cf
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 44 deletions.
6 changes: 6 additions & 0 deletions src/routes/route_check/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,16 @@ export interface Junction {
}

export interface JunctionAssessment {
arms: Arm[];
movements: Movement[];
notes: string;
}

export interface Arm {
point: Position;
name: string;
}

export type MovementKind =
| "cycling-straight"
| "cycling-left-turn"
Expand Down
2 changes: 2 additions & 0 deletions src/routes/route_check/jat_check/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@
{
name: "",
existing: {
arms: [],
movements: [],
notes: "",
},
proposed: {
arms: [],
movements: [],
notes: "",
},
Expand Down
209 changes: 165 additions & 44 deletions src/routes/route_check/jat_check/JunctionMap.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
WarningButton,
DefaultButton,
TextArea,
Radio,
TextInput,
} from "govuk-svelte";
import { onMount, onDestroy } from "svelte";
import { bbox, StreetView, MapLibreMap, BlueskyKey, Popup } from "$lib/map";
Expand All @@ -19,10 +21,12 @@
hoverStateFilter,
type LayerClickInfo,
SymbolLayer,
CircleLayer,
} from "svelte-maplibre";
import type { MapMouseEvent, Map } from "maplibre-gl";
import {
state,
type Arm,
type Movement,
type Position,
type State,
Expand All @@ -35,8 +39,12 @@
let map: Map;
let editing: number | null = null;
let hoveringSidebar: number | null = null;
type Kind = "arm" | "movement";
type ID = { kind: Kind; idx: number };
let newKind: Kind = "arm";
let editing: ID | null = null;
let hoveringSidebar: ID | null = null;
let streetviewOn = false;
$: hoverGj = getHoverData($state, editing, hoveringSidebar);
Expand All @@ -50,23 +58,29 @@
function getHoverData(
state: State,
editing: number | null,
hoveringSidebar: number | null,
editing: ID | null,
hoveringSidebar: ID | null,
): FeatureCollection {
let gj: FeatureCollection = {
type: "FeatureCollection" as const,
features: [],
};
let id = editing ?? hoveringSidebar;
if (id != null) {
gj.features.push(
lineFeature(state.jat[junctionIdx][stage].movements[id], id),
);
if (id.kind == "arm") {
gj.features.push(
armFeature(state.jat[junctionIdx][stage].arms[id.idx], id.idx),
);
} else {
gj.features.push(
lineFeature(state.jat[junctionIdx][stage].movements[id.idx], id.idx),
);
}
}
return gj;
}
function select(id: number) {
function select(id: ID) {
editing = id;
hoveringSidebar = null;
}
Expand All @@ -89,25 +103,42 @@
return;
}
$state.jat[junctionIdx][stage].movements = [
...$state.jat[junctionIdx][stage].movements,
{
point1: e.lngLat.toArray() as Position,
// Offset 10 meters to the north
point2: destination(e.lngLat.toArray(), 0.01, 0).geometry
.coordinates as Position,
kind: "cycling-straight",
score: "X",
name: "",
notes: "",
},
];
editing = $state.jat[junctionIdx][stage].movements.length - 1;
if (newKind == "arm") {
$state.jat[junctionIdx][stage].arms = [
...$state.jat[junctionIdx][stage].arms,
{
point: e.lngLat.toArray() as Position,
name: "",
},
];
editing = {
kind: "arm",
idx: $state.jat[junctionIdx][stage].arms.length - 1,
};
} else {
$state.jat[junctionIdx][stage].movements = [
...$state.jat[junctionIdx][stage].movements,
{
point1: e.lngLat.toArray() as Position,
// Offset 10 meters to the north
point2: destination(e.lngLat.toArray(), 0.01, 0).geometry
.coordinates as Position,
kind: "cycling-straight",
score: "X",
name: "",
notes: "",
},
];
editing = {
kind: "movement",
idx: $state.jat[junctionIdx][stage].movements.length - 1,
};
}
hoveringSidebar = null;
}
function onFeatureClick(e: CustomEvent<LayerClickInfo>) {
select(e.detail.features[0].id as number);
select({ kind: "movement", idx: e.detail.features[0].id as number });
}
// TODO Wait for loaded
Expand All @@ -119,19 +150,27 @@
map.off("click", onMapClick);
});
function toGj(movements: Movement[]): FeatureCollection {
// This includes movements and arms for zooming. When rendered, arms aren't shown.
function toGj(state: State): FeatureCollection {
let gj = {
type: "FeatureCollection" as const,
features: movements.map((movement, idx) => lineFeature(movement, idx)),
features: state.jat[junctionIdx][stage].movements.map((movement, idx) =>
lineFeature(movement, idx),
),
};
for (let m of movements) {
for (let m of state.jat[junctionIdx][stage].movements) {
gj.features.push(arrowFeature(m, gj.features.length));
// Arrows at both ends
if (m.kind == "pedestrian") {
let opposite = { ...m, point1: m.point2, point2: m.point1 };
gj.features.push(arrowFeature(opposite, gj.features.length));
}
}
for (let arm of state.jat[junctionIdx][stage].arms) {
gj.features.push(armFeature(arm, gj.features.length));
}
return gj;
}
Expand Down Expand Up @@ -166,21 +205,50 @@
},
};
}
function armFeature(arm: Arm, id: number): Feature {
return {
type: "Feature",
id,
properties: {
kind: "arm",
},
geometry: {
type: "Point",
coordinates: arm.point,
},
};
}
function deleteMovement() {
function deleteItem() {
// TODO Modal
if (!window.confirm("Delete this movement?")) {
if (!window.confirm(`Delete this ${editing!.kind}?`)) {
return;
}
$state.jat[junctionIdx][stage].movements.splice(editing!, 1);
$state.jat[junctionIdx][stage].movements =
$state.jat[junctionIdx][stage].movements;
if (editing!.kind == "movement") {
$state.jat[junctionIdx][stage].movements.splice(editing!.idx, 1);
$state.jat[junctionIdx][stage].movements =
$state.jat[junctionIdx][stage].movements;
} else {
$state.jat[junctionIdx][stage].arms.splice(editing!.idx, 1);
$state.jat[junctionIdx][stage].arms = $state.jat[junctionIdx][stage].arms;
}
editing = null;
}
function deleteArm() {
// TODO Modal
if (!window.confirm("Delete this arm?")) {
return;
}
$state.jat[junctionIdx][stage].arms.splice(editing!.idx, 1);
$state.jat[junctionIdx][stage].arms = $state.jat[junctionIdx][stage].arms;
editing = null;
}
function zoom(animate: boolean) {
if ($state.jat[junctionIdx][stage].movements.length > 0) {
map.fitBounds(bbox(toGj($state.jat[junctionIdx][stage].movements)), {
let gj = toGj($state);
if (gj.features.length > 0) {
map.fitBounds(bbox(gj), {
padding: 20,
animate,
});
Expand All @@ -192,7 +260,7 @@
e.stopPropagation();
editing = null;
} else if (editing != null && e.key == "Delete") {
deleteMovement();
deleteItem();
}
}
Expand Down Expand Up @@ -229,13 +297,39 @@
<StreetView {map} bind:enabled={streetviewOn} />
{/if}

<Radio
legend="Add to map"
choices={[
["arm", "Arm"],
["movement", "Movement"],
]}
inlineSmall
bind:value={newKind}
/>

<h3>Arms</h3>
<ul>
{#each $state.jat[junctionIdx][stage].arms as arm, idx}
<li>
<SecondaryButton
on:click={() => select({ kind: "arm", idx })}
on:mouseenter={() => (hoveringSidebar = { kind: "arm", idx })}
on:mouseleave={() => (hoveringSidebar = null)}
>
{idx} - {arm.name || "Unnamed arm"}
</SecondaryButton>
</li>
{/each}
</ul>

<h3>Movements</h3>
<ol>
{#each $state.jat[junctionIdx][stage].movements as movement, idx}
<li>
<SecondaryButton
on:click={() => select(idx)}
on:mouseenter={() => (hoveringSidebar = idx)}
on:click={() => select({ kind: "movement", idx })}
on:mouseenter={() =>
(hoveringSidebar = { kind: "movement", idx })}
on:mouseleave={() => (hoveringSidebar = null)}
>
{movement.name || "Unnamed movement"}
Expand All @@ -247,30 +341,50 @@
<p>Total JAT score: {totalScore($state.jat[junctionIdx][stage])}%</p>
{:else}
<DefaultButton on:click={() => (editing = null)}>Save</DefaultButton>
<WarningButton on:click={deleteMovement}>Delete</WarningButton>
<Form {junctionIdx} {stage} idx={editing} />
<WarningButton on:click={deleteItem}>Delete</WarningButton>
{#if editing.kind == "movement"}
<Form {junctionIdx} {stage} idx={editing.idx} />
{:else}
<TextInput
label="Name"
bind:value={$state.jat[junctionIdx][stage].arms[editing.idx].name}
/>
{/if}
{/if}
</div>
<div style="position: relative; width: 100%">
<MapLibreMap bind:map>
{#each $state.jat[junctionIdx][stage].arms as arm, idx}
<Marker
draggable
bind:lngLat={arm.point}
on:dragend={() => select({ kind: "arm", idx })}
on:click={() => select({ kind: "arm", idx })}
>
<span class="dot" style:background-color="white">
{idx} - {arm.name}
</span>
</Marker>
{/each}

{#each $state.jat[junctionIdx][stage].movements as movement, idx}
<Marker
draggable
bind:lngLat={movement.point1}
on:dragend={() => select(idx)}
on:click={() => select(idx)}
on:dragend={() => select({ kind: "movement", idx })}
on:click={() => select({ kind: "movement", idx })}
>
<span
class="dot"
style={`background-color: ${scoreColors[movement.score]}`}
style:background-color={scoreColors[movement.score]}
style:opacity={movement.kind == "pedestrian" ? "0%" : "100%"}
/>
</Marker>
<Marker
draggable
bind:lngLat={movement.point2}
on:dragend={() => select(idx)}
on:click={() => select(idx)}
on:dragend={() => select({ kind: "movement", idx })}
on:click={() => select({ kind: "movement", idx })}
>
<span
class="dot"
Expand All @@ -280,7 +394,7 @@
</Marker>
{/each}

<GeoJSON data={toGj($state.jat[junctionIdx][stage].movements)}>
<GeoJSON data={toGj($state)}>
<!-- TODO Two layers due to https://github.com/maplibre/maplibre-gl-js/issues/1235 -->
<LineLayer
id="jat-cycling"
Expand Down Expand Up @@ -339,6 +453,13 @@

<GeoJSON data={hoverGj}>
<LineLayer paint={{ "line-width": 15, "line-color": "yellow" }} />
<CircleLayer
filter={["==", ["get", "kind"], "arm"]}
paint={{
"circle-color": "yellow",
"circle-radius": 20,
}}
/>
</GeoJSON>

<GeoreferenceLayer {map} />
Expand Down

0 comments on commit 1d215cf

Please sign in to comment.