⚡️ Reduce the refresh on passages update to the TtwPassage component
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { createContext, createSignal, JSX } from 'solid-js';
|
import { batch, createContext, createSignal, JSX } from 'solid-js';
|
||||||
import { createStore } from 'solid-js/store';
|
import { createStore } from 'solid-js/store';
|
||||||
|
|
||||||
import { Line, Lines, Passage, Passages, Stop, Stops } from './types';
|
import { Line, Lines, Passage, Passages, Stop, Stops } from './types';
|
||||||
@@ -7,8 +7,11 @@ import { Line, Lines, Passage, Passages, Stop, Stops } from './types';
|
|||||||
export interface BusinessDataStore {
|
export interface BusinessDataStore {
|
||||||
getLine: (lineId: string) => Promise<Line>;
|
getLine: (lineId: string) => Promise<Line>;
|
||||||
getLinePassages: (lineId: string) => Record<string, Passage[]>;
|
getLinePassages: (lineId: string) => Record<string, Passage[]>;
|
||||||
|
getLineDestinations: (lineId: string) => string[];
|
||||||
|
getDestinationPassages: (lineId: string, destination: string) => Passage[];
|
||||||
|
|
||||||
passages: () => Passages;
|
passages: () => Passages;
|
||||||
|
getPassagesLineIds: () => string[];
|
||||||
refreshPassages: (stopId: number) => Promise<void>;
|
refreshPassages: (stopId: number) => Promise<void>;
|
||||||
addPassages: (passages: Passages) => void;
|
addPassages: (passages: Passages) => void;
|
||||||
clearPassages: () => void;
|
clearPassages: () => void;
|
||||||
@@ -46,12 +49,24 @@ export function BusinessDataProvider(props: { children: JSX.Element }) {
|
|||||||
|
|
||||||
const getLinePassages = (lineId: string): Record<string, Passage[]> => {
|
const getLinePassages = (lineId: string): Record<string, Passage[]> => {
|
||||||
return store.passages[lineId];
|
return store.passages[lineId];
|
||||||
};
|
}
|
||||||
|
|
||||||
|
const getLineDestinations = (lineId: string): string[] => {
|
||||||
|
return Object.keys(store.passages[lineId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDestinationPassages = (lineId: string, destination: string): Passage[] => {
|
||||||
|
return store.passages[lineId][destination];
|
||||||
|
}
|
||||||
|
|
||||||
const passages = (): Passages => {
|
const passages = (): Passages => {
|
||||||
return store.passages;
|
return store.passages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getPassagesLineIds = (): string[] => {
|
||||||
|
return Object.keys(store.passages);
|
||||||
|
}
|
||||||
|
|
||||||
const _cleanupPassages = (passages: Passages): void => {
|
const _cleanupPassages = (passages: Passages): void => {
|
||||||
const deadline = Math.floor(Date.now() / 1000) - 60;
|
const deadline = Math.floor(Date.now() / 1000) - 60;
|
||||||
for (const linePassages of Object.values(passages)) {
|
for (const linePassages of Object.values(passages)) {
|
||||||
@@ -64,7 +79,6 @@ export function BusinessDataProvider(props: { children: JSX.Element }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
linePassages[destination] = cleaned;
|
linePassages[destination] = cleaned;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,20 +93,45 @@ export function BusinessDataProvider(props: { children: JSX.Element }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const addPassages = (passages: Passages): void => {
|
const addPassages = (passages: Passages): void => {
|
||||||
const storePassages = store.passages;
|
batch(() => {
|
||||||
for (const lineId of Object.keys(passages)) {
|
const storePassages = store.passages;
|
||||||
const newLinePassages = passages[lineId];
|
for (const lineId of Object.keys(passages)) {
|
||||||
const linePassages = storePassages[lineId];
|
const newLinePassages = passages[lineId];
|
||||||
if (linePassages === undefined) {
|
const linePassages = storePassages[lineId];
|
||||||
setStore('passages', lineId, newLinePassages);
|
if (linePassages === undefined) {
|
||||||
}
|
setStore('passages', lineId, newLinePassages);
|
||||||
else {
|
}
|
||||||
for (const destination of Object.keys(newLinePassages)) {
|
else {
|
||||||
const newLinePassagesDestination = newLinePassages[destination];
|
for (const destination of Object.keys(newLinePassages)) {
|
||||||
setStore('passages', lineId, destination, newLinePassagesDestination);
|
const newLinePassagesDestination = newLinePassages[destination];
|
||||||
|
const linePassagesDestination = linePassages[destination];
|
||||||
|
if (linePassagesDestination === undefined) {
|
||||||
|
setStore('passages', lineId, destination, newLinePassagesDestination);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (linePassagesDestination.length - newLinePassagesDestination.length != 0) {
|
||||||
|
console.log(`Server provides ${newLinePassagesDestination.length} passages, \
|
||||||
|
${linePassagesDestination.length} here... refresh all them.`);
|
||||||
|
setStore('passages', lineId, destination, newLinePassagesDestination);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
linePassagesDestination.forEach((passage, index) => {
|
||||||
|
const newPassage = newLinePassagesDestination[index];
|
||||||
|
if (passage.expectedDepartTs != newPassage.expectedDepartTs) {
|
||||||
|
console.log(`Refresh expectedDepartTs (${passage.expectedDepartTs} -> ${newPassage.expectedDepartTs}`);
|
||||||
|
setStore('passages', lineId, destination, index, 'expectedDepartTs', newPassage.expectedDepartTs);
|
||||||
|
}
|
||||||
|
if (passage.expectedArrivalTs != newPassage.expectedArrivalTs) {
|
||||||
|
console.log(`Refresh expectedArrivalTs (${passage.expectedArrivalTs} -> ${newPassage.expectedArrivalTs}`);
|
||||||
|
setStore('passages', lineId, destination, index, 'expectedArrivalTs', newPassage.expectedArrivalTs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearPassages = (): void => {
|
const clearPassages = (): void => {
|
||||||
@@ -124,7 +163,8 @@ export function BusinessDataProvider(props: { children: JSX.Element }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BusinessDataContext.Provider value={{
|
<BusinessDataContext.Provider value={{
|
||||||
getLine, getLinePassages, passages, refreshPassages, addPassages, clearPassages, getStop, searchStopByName
|
getLine, getLinePassages, getLineDestinations, getDestinationPassages, passages, getPassagesLineIds,
|
||||||
|
refreshPassages, addPassages, clearPassages, getStop, searchStopByName
|
||||||
}}>
|
}}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</BusinessDataContext.Provider>
|
</BusinessDataContext.Provider>
|
||||||
|
@@ -202,7 +202,7 @@ const Body: ParentComponent<{ maxPassagesPerPanel: number, syncPeriodMsec: numbe
|
|||||||
return <div />;
|
return <div />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { getLinePassages, passages, clearPassages, refreshPassages } = businessDataStore;
|
const { getLinePassages, getLineDestinations, passages, getPassagesLineIds, clearPassages, refreshPassages } = businessDataStore;
|
||||||
const { isPassagesRefreshEnabled, getDisplayedPanelId, setDisplayedPanelId, getPanels, setPanels } = passagesDisplayStore;
|
const { isPassagesRefreshEnabled, getDisplayedPanelId, setDisplayedPanelId, getPanels, setPanels } = passagesDisplayStore;
|
||||||
const { getDisplayedStops } = searchStore;
|
const { getDisplayedStops } = searchStore;
|
||||||
|
|
||||||
@@ -253,31 +253,29 @@ const Body: ParentComponent<{ maxPassagesPerPanel: number, syncPeriodMsec: numbe
|
|||||||
let positioneds: PositionnedPanel[] = [];
|
let positioneds: PositionnedPanel[] = [];
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
|
||||||
let chunk: Record<string, Record<string, Passage[]>> = {};
|
let lineIds: string[] = [];
|
||||||
let chunkSize = 0;
|
let destinationsNb = 0;
|
||||||
|
|
||||||
for (const lineId of Object.keys(passages())) {
|
for (const lineId of getPassagesLineIds()) {
|
||||||
const byLinePassages = getLinePassages(lineId);
|
const lineDestinations = getLineDestinations(lineId);
|
||||||
const byLinePassagesKeys = Object.keys(byLinePassages);
|
|
||||||
|
|
||||||
if (byLinePassagesKeys.length <= props.maxPassagesPerPanel - chunkSize) {
|
if (lineDestinations.length <= props.maxPassagesPerPanel - destinationsNb) {
|
||||||
chunk[lineId] = byLinePassages;
|
lineIds.push(lineId);
|
||||||
chunkSize += byLinePassagesKeys.length;
|
destinationsNb += lineDestinations.length;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const panelid = index++;
|
const panelid = index++;
|
||||||
const panel = <PassagesPanel show={panelid == getDisplayedPanelId()} passages={chunk} />;
|
const panel = <PassagesPanel stopId={getDisplayedStops()[0].id} lineIds={lineIds} show={panelid == getDisplayedPanelId()} />;
|
||||||
newPanels.push(panel);
|
newPanels.push(panel);
|
||||||
positioneds.push({ position: panelid, panel: panel });
|
positioneds.push({ position: panelid, panel: panel });
|
||||||
|
|
||||||
chunk = {};
|
lineIds = [lineId];
|
||||||
chunk[lineId] = byLinePassages;
|
destinationsNb = lineDestinations.length;
|
||||||
chunkSize = byLinePassagesKeys.length;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (chunkSize) {
|
if (destinationsNb) {
|
||||||
const panelId = index++;
|
const panelId = index++;
|
||||||
const panel = <PassagesPanel show={panelId == getDisplayedPanelId()} passages={chunk} />;
|
const panel = <PassagesPanel stopId={getDisplayedStops()[0].id} lineIds={lineIds} show={panelId == getDisplayedPanelId()} />;
|
||||||
newPanels.push(panel);
|
newPanels.push(panel);
|
||||||
positioneds.push({ position: panelId, panel: panel });
|
positioneds.push({ position: panelId, panel: panel });
|
||||||
}
|
}
|
||||||
|
@@ -1,41 +1,15 @@
|
|||||||
import { VoidComponent, createEffect, createResource, createSignal, ParentComponent, ParentProps, Show, useContext, For } from 'solid-js';
|
import { VoidComponent, createResource, ParentComponent, ParentProps, Show, useContext, For } from 'solid-js';
|
||||||
import { createDateNow, getTime } from '@solid-primitives/date';
|
import { createDateNow, getTime } from '@solid-primitives/date';
|
||||||
import { AnimationOptions } from '@motionone/types';
|
import { AnimationOptions } from '@motionone/types';
|
||||||
import { Motion } from "@motionone/solid";
|
import { Motion } from "@motionone/solid";
|
||||||
|
|
||||||
import { Line, Passage, Passages, TrafficStatus } from './types';
|
import { Line, TrafficStatus } from './types';
|
||||||
import { renderLineTransportMode, renderLinePicto } from './utils';
|
import { renderLineTransportMode, renderLinePicto } from './utils';
|
||||||
import { BusinessDataContext, BusinessDataStore } from "./businessData";
|
import { BusinessDataContext, BusinessDataStore } from "./businessData";
|
||||||
|
|
||||||
import styles from './passagesPanel.module.css';
|
import styles from './passagesPanel.module.css';
|
||||||
|
|
||||||
|
|
||||||
const TtwPassage: VoidComponent<{ passage: Passage, style: string, fontSize: number }> = (props) => {
|
|
||||||
|
|
||||||
const [dateNow] = createDateNow(5000);
|
|
||||||
|
|
||||||
const refTs = props.passage.expectedDepartTs !== null ? props.passage.expectedDepartTs : props.passage.expectedArrivalTs;
|
|
||||||
const ttwSec = refTs - (getTime(dateNow()) / 1000);
|
|
||||||
const isApproaching = ttwSec <= 60;
|
|
||||||
|
|
||||||
const transition: AnimationOptions = { duration: 3, repeat: Infinity };
|
|
||||||
return (
|
|
||||||
<div class={props.style}>
|
|
||||||
<svg viewBox={`0 0 215 ${props.fontSize}`}>
|
|
||||||
<Motion.text
|
|
||||||
x="100%" y="55%"
|
|
||||||
dominant-baseline="middle" text-anchor="end"
|
|
||||||
font-size={props.fontSize} style={{ fill: "#000000" }}
|
|
||||||
initial={isApproaching ? undefined : false}
|
|
||||||
animate={{ opacity: [1, 0, 1] }}
|
|
||||||
transition={transition}>
|
|
||||||
{Math.floor(ttwSec / 60)} min
|
|
||||||
</Motion.text>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const UnavailablePassage: VoidComponent<{ style: string }> = (props) => {
|
const UnavailablePassage: VoidComponent<{ style: string }> = (props) => {
|
||||||
const textStyle = { fill: "#000000" };
|
const textStyle = { fill: "#000000" };
|
||||||
|
|
||||||
@@ -50,8 +24,47 @@ const UnavailablePassage: VoidComponent<{ style: string }> = (props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TtwPassage: VoidComponent<{ line: Line, destination: string, index: number, style: string, fontSize: number, fallbackStyle: string }> = (props) => {
|
||||||
|
|
||||||
|
const businessDataContext: BusinessDataStore | undefined = useContext(BusinessDataContext);
|
||||||
|
if (businessDataContext === undefined)
|
||||||
|
return <div />;
|
||||||
|
|
||||||
|
const { getDestinationPassages } = businessDataContext;
|
||||||
|
|
||||||
|
const [dateNow] = createDateNow(10000);
|
||||||
|
|
||||||
|
const transition: AnimationOptions = { duration: 3, repeat: Infinity };
|
||||||
|
|
||||||
|
return (() => {
|
||||||
|
const passage = getDestinationPassages(props.line.id, props.destination)[props.index];
|
||||||
|
|
||||||
|
const refTs = passage !== undefined ? (passage.expectedDepartTs !== null ? passage.expectedDepartTs : passage.expectedArrivalTs) : 0;
|
||||||
|
const ttwSec = refTs - (getTime(dateNow()) / 1000);
|
||||||
|
const isApproaching = ttwSec <= 60;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={passage !== undefined} fallback=<UnavailablePassage style={props.fallbackStyle} />>
|
||||||
|
<div class={props.style}>
|
||||||
|
<svg viewBox={`0 0 215 ${props.fontSize}`}>
|
||||||
|
<Motion.text
|
||||||
|
x="100%" y="55%"
|
||||||
|
dominant-baseline="middle" text-anchor="end"
|
||||||
|
font-size={props.fontSize} style={{ fill: "#000000" }}
|
||||||
|
initial={isApproaching ? undefined : false}
|
||||||
|
animate={{ opacity: [1, 0, 1] }}
|
||||||
|
transition={transition}>
|
||||||
|
{Math.floor(ttwSec / 60)} min
|
||||||
|
</Motion.text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/* TODO: Manage end of service */
|
/* TODO: Manage end of service */
|
||||||
const DestinationPassages: VoidComponent<{ passages: Passage[], line: Line, destination: string }> = (props) => {
|
const DestinationPassages: VoidComponent<{ line: Line, destination: string }> = (props) => {
|
||||||
|
|
||||||
/* TODO: Find where to get data to compute traffic status. */
|
/* TODO: Find where to get data to compute traffic status. */
|
||||||
const trafficStatusColor = new Map<TrafficStatus, string>([
|
const trafficStatusColor = new Map<TrafficStatus, string>([
|
||||||
@@ -62,10 +75,6 @@ const DestinationPassages: VoidComponent<{ passages: Passage[], line: Line, dest
|
|||||||
[TrafficStatus.BYPASSED, "#ffffff"]
|
[TrafficStatus.BYPASSED, "#ffffff"]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const passagesLength = props.passages.length;
|
|
||||||
const firstPassage = passagesLength > 0 ? props.passages[0] : undefined;
|
|
||||||
const secondPassage = passagesLength > 1 ? props.passages[1] : undefined;
|
|
||||||
|
|
||||||
// TODO: Manage traffic status
|
// TODO: Manage traffic status
|
||||||
// const trafficStatusStyle = { fill: trafficStatusColor.get(props.line.trafficStatus) };
|
// const trafficStatusStyle = { fill: trafficStatusColor.get(props.line.trafficStatus) };
|
||||||
const trafficStatusStyle = { fill: trafficStatusColor.get(TrafficStatus.UNKNOWN) };
|
const trafficStatusStyle = { fill: trafficStatusColor.get(TrafficStatus.UNKNOWN) };
|
||||||
@@ -88,17 +97,15 @@ const DestinationPassages: VoidComponent<{ passages: Passage[], line: Line, dest
|
|||||||
<circle cx="50%" cy="50%" r="24" stroke="#231f20" stroke-width="3" style={trafficStatusStyle} />
|
<circle cx="50%" cy="50%" r="24" stroke="#231f20" stroke-width="3" style={trafficStatusStyle} />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<Show when={firstPassage !== undefined} fallback=<UnavailablePassage style={styles.unavailableFirstPassage} />>
|
<TtwPassage line={props.line} destination={props.destination} index={0} style={styles.firstPassage}
|
||||||
<TtwPassage style={styles.firstPassage} passage={firstPassage} fontSize={50} />
|
fontSize={50} fallbackStyle={styles.unavailableFirstPassage} />
|
||||||
</Show>
|
<TtwPassage line={props.line} destination={props.destination} index={1} style={styles.secondPassage}
|
||||||
<Show when={secondPassage !== undefined} fallback=<UnavailablePassage style={styles.unavailableSecondPassage} />>
|
fontSize={45} fallbackStyle={styles.unavailableSecondPassage} />
|
||||||
<TtwPassage style={styles.secondPassage} passage={secondPassage} fontSize={45} />
|
|
||||||
</Show>
|
|
||||||
</div >
|
</div >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PassagesPanelComponentProps = ParentProps & { passages: Passages, show: boolean };
|
export type PassagesPanelComponentProps = ParentProps & { stopId: number, lineIds: string[], show: boolean };
|
||||||
export type PassagesPanelComponent = ParentComponent<PassagesPanelComponentProps>;
|
export type PassagesPanelComponent = ParentComponent<PassagesPanelComponentProps>;
|
||||||
export const PassagesPanel: PassagesPanelComponent = (props) => {
|
export const PassagesPanel: PassagesPanelComponent = (props) => {
|
||||||
|
|
||||||
@@ -106,29 +113,23 @@ export const PassagesPanel: PassagesPanelComponent = (props) => {
|
|||||||
if (businessDataContext === undefined)
|
if (businessDataContext === undefined)
|
||||||
return <div />;
|
return <div />;
|
||||||
|
|
||||||
const { getLine } = businessDataContext;
|
const { getLine, getLineDestinations } = businessDataContext;
|
||||||
|
|
||||||
const getLines = async (lineIds: string[]): Promise<Line[]> => {
|
const getLines = async (lineIds: string[]): Promise<Line[]> => {
|
||||||
const lines = await Promise.all<Promise<Line>[]>(lineIds.map((lineId) => getLine(lineId)));
|
const lines = await Promise.all<Promise<Line>[]>(lineIds.map((lineId) => getLine(lineId)));
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
const [lines] = createResource<Line[], string[]>(props.lineIds, getLines);
|
||||||
const [lineIds, setLinesIds] = createSignal<string[]>([]);
|
|
||||||
const [lines] = createResource<Line[], string[]>(lineIds, getLines);
|
|
||||||
|
|
||||||
createEffect(async () => {
|
|
||||||
setLinesIds(Object.keys(props.passages));
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div classList={{ [styles.passagesContainer]: true, [styles.displayed]: props.show }} >
|
<div classList={{ [styles.passagesContainer]: true, [styles.displayed]: props.show }} >
|
||||||
<Show when={lines() !== undefined} >
|
<Show when={lines() !== undefined} >
|
||||||
<For each={lines()}>
|
<For each={lines()}>
|
||||||
{(line) =>
|
{(line) =>
|
||||||
<Show when={props.passages[line.id]}>
|
<Show when={getLineDestinations(line.id) !== undefined}>
|
||||||
<For each={Object.keys(props.passages[line.id])}>
|
<For each={getLineDestinations(line.id)}>
|
||||||
{(destination) =>
|
{(destination) =>
|
||||||
<DestinationPassages passages={props.passages[line.id][destination]} line={line} destination={destination} />
|
<DestinationPassages line={line} destination={destination} />
|
||||||
}
|
}
|
||||||
</For>
|
</For>
|
||||||
</Show>
|
</Show>
|
||||||
|
Reference in New Issue
Block a user