Skip to content

Commit

Permalink
minor: add charge icons and split up interrupted rides into coloured …
Browse files Browse the repository at this point in the history
…segments
  • Loading branch information
acheronfail committed Oct 3, 2024
1 parent fae1400 commit fae1921
Show file tree
Hide file tree
Showing 9 changed files with 425 additions and 349 deletions.
11 changes: 11 additions & 0 deletions src/assets/charge-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
558 changes: 279 additions & 279 deletions src/assets/demo.csv

Large diffs are not rendered by default.

43 changes: 30 additions & 13 deletions src/components/App.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import Chart from './Chart.svelte';
import Map, { type FaultPoint } from './Map.svelte';
import Map, { type PointOfInterest } from './Map.svelte';
import Details from './Details.svelte';
import Header from './Header.svelte';
import { demoFile, demoRows } from '../lib/parse/float-control';
Expand All @@ -16,6 +16,8 @@
import { parse, supportedMimeTypes } from '../lib/parse';
import { globalState } from '../lib/global.svelte';
const RIDE_GAP_THRESHOLD_SECONDS = 60;
/** source of data*/
let source = $state(DataSource.None);
/** selected file */
Expand All @@ -34,7 +36,7 @@
const curr = rows[i]!;
gpsPoints.push([curr.gps_latitude, curr.gps_longitude]);
if (prev && curr.time - prev.time > 60) {
if (prev && curr.time - prev.time > RIDE_GAP_THRESHOLD_SECONDS) {
gpsGaps.push(i);
}
}
Expand Down Expand Up @@ -63,28 +65,43 @@
return { gpsPoints, gpsGaps };
});
/** entire list of faults from `rows` */
let faultPoints = $derived.by(() => {
const points: FaultPoint[] = [];
/** entire list of states from `rows` */
let pointsOfInterest = $derived.by(() => {
const points: PointOfInterest[] = [];
for (let i = 0; i < rows.length; i++) {
const row = rows[i]!;
let fault: string | undefined;
let states: string[] = [];
// fault from VESC
if (row.state !== 'riding') {
fault = row.state;
} else if (row.speed > 2) {
states.push(row.state);
}
// custom footpad faults
if (row.speed > 2) {
const combinedAdcVoltage = row.adc1 + row.adc2;
if (combinedAdcVoltage < 2) {
fault = State.Custom_NoFootpadsAtSpeed;
states.push(State.Custom_NoFootpadsAtSpeed);
} else if (combinedAdcVoltage < 4) {
fault = State.Custom_OneFootpadAtSpeed;
states.push(State.Custom_OneFootpadAtSpeed);
}
}
// inferred charge points
const prevRow = rows[i - 1];
if (prevRow !== undefined) {
if (row.time - prevRow.time > RIDE_GAP_THRESHOLD_SECONDS && row.voltage > prevRow.voltage) {
states.push(State.Custom_ChargePoint);
}
}
// SAFETY: since the enum is non-exhaustive, just check it's not in here so
// any ones we don't know about are shown
if (fault && !settings.hiddenFaults.includes(fault as State)) {
points.push({ index: i, fault });
for (const state of states) {
if (!settings.hiddenStates.includes(state as State)) {
points.push({ index: i, state });
}
}
}
Expand Down Expand Up @@ -273,7 +290,7 @@
wide:relative wide:top-[unset] wide:h-[unset] wide:border-b-0"
class:map-swapped={swapMapAndDetails}
>
<Map {setSelectedIdx} {setVisible} {selectedRowIndex} {visibleRows} {gpsPoints} {gpsGaps} {faultPoints} />
<Map {setSelectedIdx} {setVisible} {selectedRowIndex} {visibleRows} {gpsPoints} {gpsGaps} {pointsOfInterest} />
</div>
<div
class="place-self-center w-full h-full overflow-hidden
Expand Down
38 changes: 20 additions & 18 deletions src/components/Map.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
import Leaflet, { type LatLngExpression } from 'leaflet';
import type { RowWithIndex } from '../lib/parse/types';
export interface FaultPoint {
export interface PointOfInterest {
index: number;
fault: string;
state: string;
}
export interface Props {
Expand All @@ -14,21 +14,22 @@
selectedRowIndex: number;
gpsPoints: LatLngExpression[];
gpsGaps: number[];
faultPoints: FaultPoint[];
pointsOfInterest: PointOfInterest[];
}
</script>

<script lang="ts">
import { untrack } from 'svelte';
import { getIcon, MapLine, riderIcon, getPolyline } from '../lib/map-helpers';
import { getIcon, MapLine, riderIcon, getPolyline, SegmentedPolyline } from '../lib/map-helpers';
let map: Leaflet.Map | null = null;
let basePolyline: Leaflet.Polyline | null = null;
let travelledPolyline: Leaflet.Polyline | null = null;
let basePolyline: SegmentedPolyline | null = null;
let travelledPolyline: SegmentedPolyline | null = null;
let riderMarker: Leaflet.Marker | null = null;
const faultMarkers: Leaflet.Marker[] = [];
const markers: Leaflet.Marker[] = [];
let { setSelectedIdx, selectedRowIndex, setVisible, visibleRows, gpsPoints, gpsGaps, faultPoints }: Props = $props();
let { setSelectedIdx, selectedRowIndex, setVisible, visibleRows, gpsPoints, gpsGaps, pointsOfInterest }: Props =
$props();
let node = $state<HTMLDivElement | undefined>();
Expand All @@ -39,8 +40,8 @@
});
$effect(() => {
if (map && faultPoints) {
updateFaultMarkers();
if (map && pointsOfInterest) {
updateMarkers();
}
});
Expand Down Expand Up @@ -92,23 +93,23 @@
},
});
function updateFaultMarkers() {
for (const marker of faultMarkers) {
function updateMarkers() {
for (const marker of markers) {
marker.remove();
}
faultMarkers.length = 0;
markers.length = 0;
// add fault markers
for (const { index, fault } of faultPoints) {
const { icon, className } = getIcon(fault);
for (const { index, state: id } of pointsOfInterest) {
const { icon, className } = getIcon(id);
const marker = Leaflet.marker(gpsPoints[index]!, {
icon,
title: fault,
title: id,
});
// SAFETY: this function is never called unless the map has been created
marker.addTo(map!);
faultMarkers.push(marker);
markers.push(marker);
const element = marker.getElement();
if (element) {
Expand Down Expand Up @@ -154,7 +155,8 @@
// add a reset zoom button
map.addControl(new ResetButton());
updateFaultMarkers();
// add markers
updateMarkers();
map.addEventListener('zoomend', setVisibleIndices);
map.addEventListener('moveend', setVisibleIndices);
Expand Down
8 changes: 4 additions & 4 deletions src/components/SettingsModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
<ul class="text-left select-none">
{#each Object.values(State) as state}
{#if state !== State.Riding}
{@const checked = !settings.hiddenFaults.includes(state)}
{@const checked = !settings.hiddenStates.includes(state)}
{@const { icon, className } = getIcon(state)}
{@const html = icon.createIcon().innerHTML.trim()}
<li class="flex flex-rol justify-start items-center gap-2">
Expand All @@ -73,10 +73,10 @@
id="map_{state}"
{checked}
onchange={() => {
if (settings.hiddenFaults.includes(state)) {
settings.hiddenFaults = settings.hiddenFaults.filter((s) => s !== state);
if (settings.hiddenStates.includes(state)) {
settings.hiddenStates = settings.hiddenStates.filter((s) => s !== state);
} else {
settings.hiddenFaults = settings.hiddenFaults.concat(state);
settings.hiddenStates = settings.hiddenStates.concat(state);
}
}}
/>
Expand Down
33 changes: 19 additions & 14 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -67,51 +67,56 @@ hr {
left: -15px;
}

/* fault icons */
.fault-icon {
/* state icons */
.state-icon {
background-color: magenta;
border-radius: 50%;
border: 1px solid black;
display: flex;
justify-content: center;
align-items: center;
}
.fault-icon.svg {
.state-icon.svg {
background-color: transparent;
border: none;
}
/* NOTE: safari won't show SVGs unless they have an explicit width/height set */
.fault-icon svg {
.state-icon svg {
width: 1rem;
height: 1rem;
}
.fault-icon.warning svg {
.state-icon.warning svg {
fill: white;
}
.fault-icon.stop_half {
.state-icon.stop_half {
background-color: grey;
}
.fault-icon.stop_full svg {
.state-icon.stop_full svg {
fill: #dc2626;
}
.fault-icon.stop_angle svg {
.state-icon.stop_angle svg {
fill: #f97316;
}
.fault-icon.wheelslip svg {
.state-icon.wheelslip svg {
fill: #eab308;
}
.fault-icon.custom__one_footpad_at_speed svg .left {
.state-icon.custom__one_footpad_at_speed svg .left {
fill: #0ea5e9;
}
.fault-icon.custom__one_footpad_at_speed svg .right {
.state-icon.custom__one_footpad_at_speed svg .right {
fill: #991b1b;
}
.fault-icon.custom__no_footpads_at_speed svg {
.state-icon.custom__no_footpads_at_speed svg {
fill: #991b1b;
}
.fault-icon.quickstop {
.state-icon.custom__charge_point svg {
fill: yellow;
stroke: black;
stroke-width: 5px;
}
.state-icon.quickstop {
background-color: #2dd4bf;
}
.fault-icon.startup {
.state-icon.startup {
background-color: green;
}
68 changes: 54 additions & 14 deletions src/lib/map-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import Leaflet, { type LatLngExpression, type PolylineOptions } from 'leaflet';
import Leaflet, { LatLng, type LatLngExpression, type PolylineOptions } from 'leaflet';
import { teal, sky, cyan, orange, lime, yellow, green, indigo, purple, fuchsia, pink, rose } from 'tailwindcss/colors';
import riderSvg from '../assets/rider-icon.svg?raw';
import chargeSvg from '../assets/charge-icon.svg?raw';
import footpadSvg from '../assets/footpad.svg?raw';
import warningSvg from '../assets/warning.svg?raw';
import { State } from './parse/types';

export { riderSvg };
export const riderIcon = Leaflet.divIcon({ className: 'rider-icon', html: riderSvg });
export const footpadIcon = Leaflet.divIcon({ className: 'fault-icon svg', html: footpadSvg, iconSize: [20, 20] });
export const genericIcon = Leaflet.divIcon({ className: 'fault-icon' });
export const chargeIcon = Leaflet.divIcon({ className: 'state-icon svg', html: chargeSvg });
export const footpadIcon = Leaflet.divIcon({ className: 'state-icon svg', html: footpadSvg, iconSize: [20, 20] });
export const genericIcon = Leaflet.divIcon({ className: 'state-icon' });
export const warningIcon = Leaflet.divIcon({
className: 'fault-icon warning svg',
className: 'state-icon warning svg',
html: warningSvg,
iconSize: [15, 15],
});
Expand All @@ -19,33 +22,70 @@ export const faultIcons: Record<string, Leaflet.DivIcon | undefined> = {
[State.StopFull]: warningIcon,
[State.Custom_OneFootpadAtSpeed]: footpadIcon,
[State.Custom_NoFootpadsAtSpeed]: footpadIcon,
[State.Custom_ChargePoint]: chargeIcon,
};

const faultToClassName = (fault: string) => fault.replace(/\W/g, '_');
export const getIcon = (fault: string): { icon: Leaflet.DivIcon; className: string } => ({
icon: faultIcons[fault] ?? genericIcon,
className: faultToClassName(fault),
const stateToClassName = (state: string) => state.replace(/\W/g, '_');
export const getIcon = (state: string): { icon: Leaflet.DivIcon; className: string } => ({
icon: faultIcons[state] ?? genericIcon,
className: stateToClassName(state),
});

export enum MapLine {
Base,
Travelled,
}

export const MapLineOptions: Record<MapLine, PolylineOptions> = {
[MapLine.Base]: { color: '#cffafe', weight: 2, dashArray: '5 5', opacity: 0.5 },
[MapLine.Travelled]: { color: '#06b6d4', weight: 4 },
const travelledLineColours = [
teal[500],
sky[500],
indigo[500],
purple[500],
fuchsia[500],
pink[500],
rose[500],
orange[500],
yellow[500],
lime[500],
green[500],
];

export const MapLineOptions: Record<MapLine, (index: number) => PolylineOptions> = {
[MapLine.Base]: () => ({ color: cyan[100], weight: 2, dashArray: '5 5', opacity: 0.5 }),
[MapLine.Travelled]: (index) => ({
color: travelledLineColours[index % travelledLineColours.length],
weight: 4,
}),
};

export class SegmentedPolyline {
constructor(private readonly polylines: Leaflet.Polyline[]) {}
remove() {
this.polylines.forEach((line) => line.remove());
}

addTo(map: Leaflet.Map): SegmentedPolyline {
this.polylines.forEach((line) => line.addTo(map));
return this;
}

getBounds() {
return this.polylines.reduce((result, line) => result.extend(line.getBounds()), this.polylines[0]!.getBounds());
}

getLatLngs() {
return this.polylines.map((line) => line.getLatLngs() as LatLng[]);
}
}

export function getPolyline(
gpsPoints: LatLngExpression[],
gpsGaps: number[],
selectedRowIndex: number,
line: MapLine,
): Leaflet.Polyline {
): SegmentedPolyline {
const limit = line === MapLine.Travelled ? selectedRowIndex : gpsPoints.length;

// TODO: add charging icons when we've detected a pause, and the battery voltage has increased
const values: LatLngExpression[][] = [];
for (let i = 0; i < gpsGaps.length; ++i) {
const start = gpsGaps[i];
Expand All @@ -54,5 +94,5 @@ export function getPolyline(
if (limit === end) break;
}

return Leaflet.polyline(values, MapLineOptions[line]);
return new SegmentedPolyline(values.map((points, index) => Leaflet.polyline(points, MapLineOptions[line](index))));
}
Loading

0 comments on commit fae1921

Please sign in to comment.